使用 Viewport 作为纹理
前言
本教程将向您介绍如何使用 Viewport 作为可应用于 3D 对象的纹理。为了做到这一点,它将引导您完成制作程序式星球的过程,如下所示:
注解
本教程没有介绍如何编写像这个星球那样的动态氛围.
本教程假定你熟悉如何设置一个基本场景, 包括: 一个 Camera, 一个 light source, 一个 Mesh Instance 与一个 Primitive Mesh, 并将一个 SpatialMaterial 应用于网格. 重点是使用 Viewport 来动态地创建可应用于网格的纹理.
在本教程中, 我们将介绍以下主题:
如何使用 Viewport 作为渲染纹理
使用equirectangular映射将纹理映射到球体
程序式行星的片段着色器技术
从a Viewport Texture 设置粗糙度贴图
设置视区
首先, 在场景中添加 Viewport .
接下来, 将 Viewport 的大小设置为 (1024, 512)
. Viewport 实际上可以是任何尺寸, 只要其宽度是高度的两倍. 宽度需要是高度的两倍, 这样图像才能准确地映射到球体上, 因为我们将使用等量矩形投影, 后面会有更多的介绍.
接下来, 禁用HDR和禁用3D. 我们不需要HDR, 因为星球的表面不会特别亮, 所以数值在 0
和 1
之间就可以了. 将使用一个 ColorRect 来渲染表面, 所以我们也不需要3D.
选择视区并添加 ColorRect 作为子项.
将锚 “Right” 和 “Bottom” 设置为 “1”, 然后确保所有边距都设置为 “0”. 这将确保 ColorRect 占用整个 Viewport.
接下来, 我们为 Shader Material 添加一个 ColorRect (ColorRect > CanvasItem > Material > Material > New ShaderMaterial
).
注解
建议本教程基本了解阴影. 但是, 即使您不熟悉着色器, 也将提供所有代码, 因此后续操作应该没有问题.
颜色矩形>画布组件>材质>材质>点击/编辑>着色器材质>着色器>”新建着色器”>点击/编辑:
shader_type canvas_item;
void fragment() {
COLOR = vec4(UV.x, UV.y, 0.5, 1.0);
}
上面的代码呈现如下所示的渐变.
现在我们有一个基础 Viewport 我们渲染到的, 我们有一个可以应用于球体的独特图像.
应用纹理
网格实例>几何体实例>几何体>材质覆盖>”新建空间材质”:
现在我们进入 Mesh Instance 并添加一个 SpatialMaterial 给它. 不需要一个特殊的 Shader Material (尽管这对于更高级的效果来说是个好主意, 比如上面例子中的大气).
网格实例>几何体实例>几何体>材质覆盖>”点击/编辑”:
打开新创建的 SpatialMaterial , 向下滚动到 “Albedo” 部分, 点击 “Texture” 属性旁边, 添加一个Albedo Texture. 这里我们将应用制作的纹理. 选择 “New ViewportTexture”
然后, 从弹出的菜单中, 选择我们先前渲染的视图.
现在, 您的球体应使用我们渲染到视区的颜色进行着色.
注意到在纹理环绕的地方形成的丑陋缝隙吗?这是因为我们是根据UV坐标来选取颜色的, 而UV坐标并不会环绕纹理. 这是二维地图投影中的一个典型问题. 游戏开发人员通常有一个二维贴图, 他们想投射到一个球体上, 但是当它环绕时, 将有接缝. 这个问题有一个优雅的解决方法, 我们将在下一节中说明.
使行星纹理
所以现在我们渲染到我们的 Viewport 它在球体上神奇地出现了. 但是我们的纹理坐标会产生一个丑陋的缝隙. 那么我们如何以一种很好的方式获得围绕球体的一系列坐标? 一种解决方案是使用在纹理域上重复的函数. sin
和 cos
是两个这样的函数. 让我们将它们应用于纹理, 看看会发生什么.
COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
不算太坏. 如果你环顾四周, 可以看到接缝现在已经消失了, 但在它所处位置, 会有两极的挤压. 这种挤压是由于Godot在其 SpatialMaterial 中把纹理映射到球体的方式造成的. 它使用了一种叫做 “等角投影” 的投影技术, 将球面图形转化为一个二维平面.
注解
如果你对技术方面的一些额外信息感兴趣,我们将从球面坐标转换为直角坐标。球面坐标映射的是球体的经度和纬度,而直角坐标则是从球体中心到点的一个向量。
对于每个像素, 我们将计算它在球体上的三维位置. 由此, 我们将使用3D噪波来确定一个颜色值. 通过计算3D噪波, 我们解决了两极的挤压问题. 要理解为什么, 想象一下在球体表面而不是在二维平面上计算噪声. 当你跨越球体表面进行计算时, 你永远不会碰到边缘, 因此你永远不会在极点上产生接缝或夹点. 下面的代码将 “UVs” 转换为笛卡尔坐标.
float theta = UV.y * 3.14159;
float phi = UV.x * 3.14159 * 2.0;
vec3 unit = vec3(0.0, 0.0, 0.0);
unit.x = sin(phi) * sin(theta);
unit.y = cos(theta) * -1.0;
unit.z = cos(phi) * sin(theta);
unit = normalize(unit);
如果我们使用 unit
作为输出 COLOR
值, 我们可以得到:
现在我们可以计算出球体表面的3D位置, 可以使用3D噪声来制作球体. 直接从 Shadertoy 中使用这个噪声函数:
vec3 hash(vec3 p) {
p = vec3(dot(p, vec3(127.1, 311.7, 74.7)),
dot(p, vec3(269.5, 183.3, 246.1)),
dot(p, vec3(113.5, 271.9, 124.6)));
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(dot(hash(i + vec3(0.0, 0.0, 0.0)), f - vec3(0.0, 0.0, 0.0)),
dot(hash(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0)),
dot(hash(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0)), u.x), u.y),
mix(mix(dot(hash(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0)),
dot(hash(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0)),
dot(hash(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z );
}
注解
所有功劳归作者Inigo Quilez所有. 它是在 MIT
许可下发布的.
现在使用 noised
, 将以下内容添加到 fragment
函数中:
float n = noise(unit * 5.0);
COLOR.xyz = vec3(n * 0.5 + 0.5);
注解
为了突出显示纹理, 我们将材质设置为无阴影.
你现在可以看到, 尽管这看起来完全不像所承诺的球体, 但噪音确实无缝地包裹着球体. 对此, 让我们进入一些更丰富多彩的东西.
着色这个星球
现在来制作行星的颜色. 虽然有很多方法可以做到这一点, 但目前, 我们将使用水和陆地之间的梯度.
要在 GLSL 中创建渐变, 我们使用 mix
函数. mix
需要两个值来插值和第三个参数来选择在它们之间插入多少, 实质上它将两个值 混合 在一起. 在其他API中, 此函数通常称为 lerp
. 虽然 lerp
通常用于将两个浮点数混合在一起, 但 mix
可以取任何值, 无论它是浮点数还是向量类型.
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), n * 0.5 + 0.5);
第一种颜色是蓝色, 代表海洋. 第二种颜色是一种偏红的颜色, 因为所有外星球都需要红色的地形. 最后, 它们 n * 0.5 + 0.5
混合在一起. n
在 -1
和 1
之间平滑变化. 所以我们把它映射到 mix
预期的 0-1
范围内. 现在你可以看到, 颜色在蓝色和红色之间变化.
这比我们想要的还要模糊一些. 行星通常在陆地和海洋之间有一个相对清晰的分隔. 为了做到这一点, 我们将把最后一项改为 smoothstep(-0.1, 0.0, n)
. 整条线就变成了这样:
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), smoothstep(-0.1, 0.0, n));
smoothstep
所做的是, 如果第三个参数低于第一个参数, 则返回 0
, 如果第三个参数大于第二个参数, 则返回 1
, 如果第三个数字在第一个和第二个之间, 则在 0
和 1
之间平滑地混合. 所以在这一行中, 当 n
小于 -0.1
时, smoothstep
返回 0
, 当 n
高于 0
时, 它返回 1
.
还有一件事, 使其更像一个行星. 这片土地不应该是圆球状的;让我们把边缘变得更粗糙一些. 在着色器中经常使用的一个技巧是在不同的频率下将不同层次的噪声叠加在一起, 使地形看起来粗糙. 我们使用一个层来制作大陆的整体球状结构. 然后, 另一层将边缘打碎, 然后是另一层, 以此类推. 我们要做的是用四行着色器代码来计算 n
, 而不是只有一行. n
变成了:
float n = noise(unit * 5.0) * 0.5;
n += noise(unit * 10.0) * 0.25;
n += noise(unit * 20.0) * 0.125;
n += noise(unit * 40.0) * 0.0625;
现在这个星球看起来像:
而在重新打开阴影后, 看起来就像:
制作海洋
让这个看起来更像是一颗行星的最后一件事. 海洋和陆地以不同的方式反射光线. 因此, 我们希望海洋比陆地更加闪耀. 我们可以通过将第四个值传递到输出 COLOR
的 alpha
通道并将其用作粗糙度图来实现.
COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);
该行对于水返回 0.3
, 对于土地返回 1.0
. 这意味着土地将变得很粗糙, 而水将变得非常光滑.
然后, 在材质中, 在 “Metallic” 部分, 确保 Metallic
设置为 0
, Specular
设置为 1
. 这样做的原因是水对光线的反射非常好, 但不是金属的. 这些值在物理上并不准确, 但对于这个演示来说已经足够好了.
接下来, 在 “Roughness” 部分, 将 Roughness
设置为 1
, 并将粗糙度纹理设置为 Viewport Texture , 指向我们的行星纹理 Viewport . 最后, 将 Texture Channel
设置为 Alpha
. 这将指示渲染器使用我们输出的 COLOR
的 alpha
通道作为 Roughness
值.
你会注意到, 除了行星不再反射天空外, 几乎没有什么变化. 这是因为默认情况下, 当某样东西以Alpha值渲染时, 它会被绘制为背景上的透明物体. 因为 Viewport 的默认背景是不透明的, 所以 Viewport Texture 的 alpha
通道是 1
, 导致行星纹理的颜色略微变淡, 并且到处的 Roughness
值都是 1
. 为了纠正这个问题, 我们转到 Viewport 并启用 “Transparent Bg” 属性. 由于现在是在另一个透明物体上渲染, 要启用 blend_premul_alpha
:
render_mode blend_premul_alpha;
这是将颜色预先乘以 alpha
值, 然后将它们正确地混合在一起. 通常情况下, 当在一个透明的颜色上混合另一个颜色时, 即使背景的 alpha
为 0
(如本例), 也会出现奇怪的颜色渗漏问题. 设置 blend_premul_alpha
可以解决这个问题.
现在这个星球看起来海洋上能够反射光线, 而不是在陆地上. 如果你还没有这样做, 在场景中添加一个 OmniLight , 这样你就可以移动它, 看到海洋上反射的效果.
这就是你的作品。使用 Viewport 生成的程序式行星。