纹理烘焙:原理及实现

纹理烘焙是计算机图形学中常见的技术,用于将着色器的细节传输到纹理中。 如果你的着色器计算量很大,但会产生静态结果,例如,这非常有用。 复杂的噪音。

NSDT在线工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器

许多建模应用程序都有此功能。 如果你有从 Blender/Houdini/Maya 传输资源的管道,那么你可能需要在那里进行纹理烘焙。 但是,如果你主要使用 Unity 进行工作,那么本教程适合你。 通过这种技术,你可以有效地保存实时操作的自定义着色器结果,并在任何其他具有默认着色器的应用程序中使用生成的纹理(Sketchfab?等)。

首先,让我们从列出问题开始:

  • 将应用了着色器的网格展开到 uv 坐标空间中。
  • 将展开的网格渲染为纹理。
  • 将纹理保存到磁盘。

仅供参考,如果你只是想查看一些代码(就像我一直做的那样),就在这里。

1、展开网格

展开网格是将 3D 顶点坐标映射到其 2D uv 坐标空间的过程。 要在顶点着色器中执行此操作,我们可以将顶点的 x 和 y 分量设置为其 uv 坐标。 将 z 分量设置为 0 将使我们在 XY 平面中 z=0 处留下展开的网格。

要尝试此操作,请复制要烘焙的着色器,并使用以下行调整顶点位置:

v.vertex = float4(v.uv.xy, 0.0, 1.0);

应用于原始网格的这个新着色器将向你显示展开的网格:

展开的unity球体,应用了 fbm 噪声着色器

解开的树,应用了 fbm 噪声着色器

2、如何将这个展开的网格渲染为纹理?

正交相机! 对于正交投影,没有深度线索。 无论距相机的距离如何,渲染图像中的对象尺寸都保持不变。 这将使我们能够绘制完美的 2D UV 网格而不失真。

透视与正交投影

要开始烘焙过程,我们需要将一个脚本附加到相机来处理烘焙逻辑。 我们将其附加到相机,因为我们想要在完成渲染场景后使用 Graphics.DrawMeshNow 绘制网格。 为此,我们需要访问 OnPostRender 函数。

在示例项目中,我在 ShaderBaker.cs 中的“M”键按下事件上发生以下代码(因为将事物绑定到随机键非常简单,哈哈)。

Mesh M = objectToBake.GetComponent<MeshFilter>().mesh;
// create a new render texture 
RenderTexture rt = RenderTexture.GetTemporary(width, height);

// set the active render target 
Graphics.SetRenderTarget(rt);
// save the last camera state
GL.PushMatrix(); 
// load an orthographic camera 
GL.LoadOrtho(); 
// set the active material to be the unwrapping material we made earlier
uvMaterial.SetPass(0);
// draw the mesh, the matrix does not matter because we are not using it for any projection in the shader
Graphics.DrawMeshNow(M, Matrix4x4.identity);
// ** save to disk  here **
// reset state
Graphics.SetRenderTarget(null);
RenderTexture.ReleaseTemporary(rt);
GL.PopMatrix();

关于上面发生的事情有很多话要说,但如何说取决于你对图形管道的熟悉程度……我想这里最有趣的两件事是加载正交相机和激活适当的 UV 展开材质 使用 SetPass 进行渲染。

UV 展开着色器将与你要烘焙的着色器相同,除了顶点着色器中的两条线更改之外。 前面提到的第一个变化是将顶点坐标重新映射到 UV 空间。

v.vertex = float4(v.uv.xy, 0.0, 1.0);

其次,我们需要将标准行:

o.vertex = UnityObjectToClipPos(v.vertex)

替换为:

o.vertex = mul(UNITY_MATRIX_P, v.vertex);

UnityObjectToClipPos 本质上是将顶点与其世界矩阵和投影矩阵相乘。 我们不关心该对象的世界矩阵,因为我们现在渲染的 UV 坐标应始终以 0 为中心。但是,我们仍然需要将 UV 坐标投影到屏幕空间,这只需使用投影矩阵 UNITY_MATRIX_P即可。

另一件需要注意的事情是,Unity 文档指出 DrawMeshNow 不包含光照信息,因此这意味着此实现仅适用于无光照着色器。 如果想要烘焙一个完全集成光照和阴影的着色器,你必须考虑使用 DrawMesh 函数。

3、将纹理保存到磁盘

此时,我们应该在渲染纹理中拥有烘焙的着色器纹理贴图。 只需要谷歌一下就能解决这个问题,因为这部分过程很常见。 这些步骤涉及将 RenderTexture 转换为 Texture2D ,然后将结果编码为 PNG。 此代码可以在 ShaderBaker.cs 中找到。

WOW! 现在我们有一个 .png 纹理代表选择的未光照着色器。 然而,像往常一样,总会出现一些根本性的问题。 如果你尝试在任何默认 Unity 材质上使用新纹理作为 _MainTex,可能会在 UV 岛边缘看到黑色伪像:

由精确 UV 接缝周围的黑色造成的伪影

这是因为纹理是使用精确的 UV 坐标烘焙的,并且没有考虑在边缘采样纹理时发生的采样方差。 为了解决这个问题,我发现其他建模软件在新烘焙的纹理上实现了“岛边界扩展”,这只是扩展边界的混合操作。

我决定通过在输出纹理上实施第二次扩张来实现“UV岛边界扩展”。

4、解决边缘伪影问题

膨胀(dilation)是一种扩展形状的形态学方法,最初是为二值(黑色或白色)图像定义的:

该算法采用结构元素(在本例中为 3x3 图块),并通过将图块置于像素中心来迭代图像的每个像素。 如果图块中有白色元素,我们会将图块转换为白色。

要将其扩展到彩色图像,我们需要一个不同的因素来转换图块。 我决定让结构元素对像素颜色与预定义背景颜色的差异进行操作。 如果结构元素中有一个像素的颜色值与背景颜色足够远,我们可以转换给定的像素。

这个实现可以在 Dilate.shader 中找到。 为了将其合并到我们现有的设置中,我们将以下几行添加到烘焙代码中:

...
Graphics.DrawMeshNow(M, Matrix4x4.identity);
Graphics.SetRenderTarget(null);
...
// create a second render target 
RenderTexture rt2 = RenderTexture.GetTemporary(width, height);
// use the dilate shader on our first render target, output to rt2
Graphics.Blit(rt, rt2, dilateMat);
// save rt2 to png
SaveTexture(rt2, objectToBake.name);
// reset 
RenderTexture.ReleaseTemporary(rt);
RenderTexture.ReleaseTemporary(rt2);
GL.PopMatrix();

膨胀前和膨胀后

这很好地扩展了纹理! 需要注意的是,这种膨胀实现仅在纹理的背景颜色与着色器的颜色不同时才有效。 我在 ShaderBaker.cs 中添加了一个属性来设置背景颜色:)

另外,接缝处仍然有非常小的伪影,我还没有弄清楚那里发生了什么。

5、结束语

由于本文中的代码相当断断续续,我强烈建议你查看 github 项目中的代码。 以下是代码的摘要:

  • UVUnwrap.shader — 此着色器与你要烘焙的着色器重复,但顶点着色器已修改为在 UV 空间中渲染顶点。
  • Dilate.shader — 此着色器负责输出纹理的膨胀后处理。
  • ShaderBaker.cs — 将此脚本附加到相机,它负责将网格渲染为纹理。 它具有公共字段,你可以在其中放置要烘焙的对象、具有要烘焙的着色器的展开版本的材质 (UVUnwrap.shader) 以及具有扩张着色器的材质 (Dilate.shader)。 你还应该将 backgroundColor 属性设置为与着色器中的颜色不同的颜色。

这应该就是全部了~~对我来说是一次有趣的探索,希望它能帮助开发者:)


原文链接:纹理烘焙原理及实现 - BimAnt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/201345.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

11月29日作业

自己封装一个矩形类(Rect)&#xff0c;拥有私有属性:宽度(width)、高度(height),定义公有成员函数: 初始化函数:void init(int w, int h) 更改宽度的函数:set_w(int w) 更改高度的函数:set_h(int h) 输出该矩形的周长和面积函数:void show() #include <iostream>using n…

Linux 进程(一)

1 操作系统 概念&#xff1a;任何计算机系统都包含一个基本的程序集合&#xff0c;称为操作系统(OS)。笼统的理解&#xff0c;操作系统包括 内核&#xff08;进程管理&#xff0c;内存管理&#xff0c;文件管理&#xff0c;驱动管理&#xff09; 其他程序&#xff08;例…

【数值计算方法(黄明游)】常微分方程初值问题的数值积分法:欧拉方法(向后Euler)【理论到程序】

文章目录 一、数值积分法1. 一般步骤2. 数值方法 二、欧拉方法&#xff08;Euler Method&#xff09;1. 向前欧拉法&#xff08;前向欧拉法&#xff09;2. 向后欧拉法&#xff08;后向欧拉法&#xff09;a. 基本理论b. 算法实现 常微分方程初值问题的数值积分法是一种通过数值方…

【Python】获取ip

要使用Python获取IP地址&#xff0c;可以使用socket库中的gethostname()函数和gethostbyname()函数。 import socketdef get_ip_address():hostname socket.gethostname()ip_address socket.gethostbyname(hostname)return ip_addressip get_ip_address() print("IP地…

Spark on yarn 模式的安装与部署

任务描述 本关任务&#xff1a; Spark on YARN 模式的安装与部署。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; Spark 部署模式的种类&#xff1b;Spark on YARN 模式的安装。 Spark 部署模式 Spark 部署模式主要分为以下几种&#xff0c;Spark Stand…

探索WebStorm 2023 Mac/win:最强大的JavaScript开发工具

在当今的软件开发领域&#xff0c;JavaScript已经成为了一种不可或缺的编程语言。而在众多的JavaScript开发工具中&#xff0c;WebStorm一直以其强大的功能和友好的用户界面脱颖而出。现在&#xff0c;我们迎来了全新的WebStorm 2023版本&#xff0c;它将带给开发者们更加出色的…

PyQt基础_008_ 按钮类控件QSpinbox

基本操作 import sys from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import *class spindemo(QWidget):def __init__(self, parentNone):super(spindemo, self).__init__(parent)self.setWindowTitle("SpinBox 例子")self.resize(300,…

BTCPay Server:免费、安全、开源的比特币支付处理器 | 开源日报 No.90

MunGell/awesome-for-beginners Stars: 58.0k License: NOASSERTION 这个项目是一个收集开源项目的列表&#xff0c;旨在帮助初学者找到可以贡献代码的机会。该列表按编程语言分类&#xff0c;并列出了每个项目以及其标签 (如 “good-first-issue”、“beginner” 等)。主要功…

使用Ray创建高效的深度学习数据管道

大家好&#xff0c;用于训练深度学习模型的GPU功能强大但价格昂贵。为了有效利用GPU&#xff0c;开发者需要一个高效的数据管道&#xff0c;以便在GPU准备好计算下一个训练步骤时尽快将数据传输到GPU&#xff0c;使用Ray可以大大提高数据管道的效率。 1.训练数据管道的结构 首…

7. 栈

栈(stack)是一种遵循先入后出的逻辑的线性数据结构。我们可以将栈类比为桌面上的一摞盘子&#xff0c;如果需要拿出底部的盘子&#xff0c;则需要先将上面的盘子依次取出。我们将盘子替换为各种类型的元素&#xff08;如整数、字符、对象等&#xff09;&#xff0c;就得到了栈数…

二叉树OJ题之二

今天我们一起来看一道判断一棵树是否为对称二叉树的题&#xff0c;力扣101题&#xff0c; https://leetcode.cn/problems/symmetric-tree/ 我们首先先来分析这道题&#xff0c;要判断这道题是否对称&#xff0c;我们首先需要判断的是这颗树根节点的左右子树是否对称&#xff0…

基于AOP的声明式事物控制

目录 Spring事务编程概述 基于xml声明式事务控制 事务属性 isolation timeout read-only propagation 全注解开发 Spring事务编程概述 事务是开发中必不可少的东西&#xff0c;使用JDBC开发时&#xff0c;我们使用connection对事务进行控制&#xff0c;使用MyBatis时&a…

算法基础之字符串哈希

字符串哈希 核心思想&#xff1a;用p(131或者13331)进制数储存字符串每一位数的hash值 L—R的哈希值 h[R]-h[L-1]*PR-L1 哈希值很大—>modQ(264)变小 用unsigned long long 存 (出界) #include<iostream>using namespace std;typedef unsigned long long ULL;co…

嵌入式八股 | 校招秋招 | 笔试面试 | 精选题目

欢迎关注微信公众号【赛博二哈】获取八股PDF 并加入嵌入式求职交流群。提供简历模板、学习路线、岗位整理等 欢迎加入知识星球【嵌入式求职星球】获取完整嵌入式八股。 提供简历修改、项目推荐、求职规划答疑。另有各城市、公司岗位、笔面难题、offer选择、薪资爆料等 嵌入式…

【知识】简单理解为何GCN层数越多越能覆盖多跳邻居聚合信息范围更广

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 背景说明 大多数博客在介绍GCN层数时候&#xff0c;都会提到如下几点(经总结)&#xff1a; 在第一层&#xff0c;节点聚合来自其直接邻居的信息。在第二层&#xff0c;由于每个节点现在包含了其直接邻居的信息&a…

如何设置Linux终端提示信息

如何设置Linux终端提示信息 1 方法一&#xff1a;只能在VSCode或者Pycharm终端显示提示信息2 方法二&#xff1a;只能在MobaXterm等远程软件上显示提示3 方法三&#xff1a;避免用户没看到上面的提示&#xff0c;上面两种都设置一下 在使用远程终端时&#xff0c;由于多用户使用…

在很多nlp数据集上超越tinybert 的新架构nlp神经网络模型

在很多nlp数据集上超越tinybert 的新架构nlp神经网络模型 网络结构图测试代码网络结构图 测试代码 import paddle import numpy as np import pandas as pd from tqdm import tqdmclass FeedFroward(paddle.nn.Layer):

园区智能配电系统(电力智能监控系统)

园区智能配电系统是一种针对园区电力配送和管理的智能化系统。它的主要功能是实时监控设备运行情况&#xff0c;进行电能质量分析&#xff0c;监控电能损耗&#xff0c;以及分时段用电统计等。 具体来说&#xff0c;园区智能配电系统可以利用现代技术如RS-485总线通信、数据库管…

一、Gradle 手动创建一个项目

文章目录 Gradle 介绍Gradle Wrapper Gradle 使用手动安装 Gradle初始化 Gradle 介绍 Gradle 是一个快速的、可信的、适应性强的自动化构建工具&#xff0c;它是开源的。它使用优雅的并且可扩展的描述性语言。其他的介绍在官网可以了解。 Gradle Wrapper 官方建议使用 Gradl…

找不到 sun.misc.BASE64Decoder ,sun.misc.BASE64Encoder 类

找不到 sun.misc.BASE64Decoder &#xff0c;sun.misc.BASE64Encoder 类 1. 现象 idea 引用报错 找不到对应的包 import sun.misc.BASE64Decoder; import sun.misc.BASE64Encoder;2. 原因 因为sun.misc.BASE64Decoder和sun.misc.BASE64Encoder是Java的内部API&#xff0c;通…
最新文章