Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

使用 Viewport 作为纹理

前言

本教程将向你介绍如何使用 Viewport 作为可应用于 3D 对象的纹理。为了做到这一点,它将引导你完成制作程序式星球的过程,如下所示:

../../_images/planet_example.png

备注

本教程没有介绍如何编写像这个星球那样的动态氛围.

This tutorial assumes you are familiar with how to set up a basic scene including: a Camera3D, a light source, a MeshInstance3D with a Primitive Mesh, and applying a StandardMaterial3D to the mesh. The focus will be on using the Viewport to dynamically create textures that can be applied to the mesh.

在本教程中, 我们将介绍以下主题:

  • 如何使用 Viewport 作为渲染纹理

  • 使用 equirectangular 映射将纹理映射到球体

  • 程序式行星的片段着色器技术

  • 视口纹理设置粗糙度贴图

设置视口

首先, 在场景中添加 Viewport .

接下来, 将 Viewport 的大小设置为 (1024, 512) . Viewport 实际上可以是任何尺寸, 只要其宽度是高度的两倍. 宽度需要是高度的两倍, 这样图像才能准确地映射到球体上, 因为我们将使用等量矩形投影, 后面会有更多的介绍.

../../_images/planet_new_viewport.png

接下来, 禁用HDR和禁用3D. 我们不需要HDR, 因为星球的表面不会特别亮, 所以数值在 01 之间就可以了. 将使用一个 ColorRect 来渲染表面, 所以我们也不需要3D.

选择视口并添加 ColorRect 作为子项.

将“Right”和“Bottom”的 Anchor 设置为 1,然后确保所有边距都设置为 0。这样就确保 ColorRect 占据了整个 Viewport

../../_images/planet_new_colorrect.png

接下来, 我们为 Shader Material 添加一个 ColorRect (ColorRect > CanvasItem > Material > Material > New ShaderMaterial).

备注

建议本教程基本了解阴影. 但是, 即使你不熟悉着色器, 也将提供所有代码, 因此后续操作应该没有问题.

ColorRect > CanvasItem > Material > Material > 点击 / 编辑 > ShaderMaterial > Shader > 新建 Shader > 点击 / 编辑:

shader_type canvas_item;

void fragment() {
    COLOR = vec4(UV.x, UV.y, 0.5, 1.0);
}

上面的代码呈现如下所示的渐变。

../../_images/planet_gradient.png

现在我们有一个基础 Viewport 我们渲染到的, 我们有一个可以应用于球体的独特图像.

应用纹理

MeshInstance3D > GeometryInstance > Geometry > Material Override > New StandardMaterial3D:

Now we go into the MeshInstance3D and add a StandardMaterial3D to it. No need for a special Shader Material (although that would be a good idea for more advanced effects, like the atmosphere in the example above).

MeshInstance3D > GeometryInstance > Geometry > Material Override > click / Edit:

Open the newly created StandardMaterial3D and scroll down to the "Albedo" section and click beside the "Texture" property to add an Albedo Texture. Here we will apply the texture we made. Choose "New ViewportTexture"

../../_images/planet_new_viewport_texture.png

然后, 从弹出的菜单中, 选择我们先前渲染的视图.

../../_images/planet_pick_viewport_texture.png

现在, 你的球体应使用我们渲染到视口的颜色进行着色.

../../_images/planet_seam.png

注意到在纹理环绕的地方形成的丑陋缝隙吗?这是因为我们是根据UV坐标来选取颜色的, 而UV坐标并不会环绕纹理. 这是二维地图投影中的一个典型问题. 游戏开发人员通常有一个二维贴图, 他们想投射到一个球体上, 但是当它环绕时, 将有接缝. 这个问题有一个优雅的解决方法, 我们将在下一节中说明.

制作行星纹理

所以现在我们渲染到我们的 Viewport 它在球体上神奇地出现了. 但是我们的纹理坐标会产生一个丑陋的缝隙. 那么我们如何以一种很好的方式获得围绕球体的一系列坐标? 一种解决方案是使用在纹理域上重复的函数. sincos 是两个这样的函数. 让我们将它们应用于纹理, 看看会发生什么.

COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
../../_images/planet_sincos.png

Not too bad. If you look around, you can see that the seam has now disappeared, but in its place, we have pinching at the poles. This pinching is due to the way Godot maps textures to spheres in its StandardMaterial3D. It uses a projection technique called equirectangular projection, which translates a spherical map onto a 2D plane.

备注

如果你对技术方面的一些额外信息感兴趣,我们将从球面坐标转换为直角坐标。球面坐标映射的是球体的经度和纬度,而直角坐标则是从球体中心到点的一个向量。

对于每个像素, 我们将计算它在球体上的三维位置. 由此, 我们将使用3D噪波来确定一个颜色值. 通过计算3D噪波, 我们解决了两极的挤压问题. 要理解为什么, 想象一下在球体表面而不是在二维平面上计算噪声. 当你跨越球体表面进行计算时, 你永远不会碰到边缘, 因此你永远不会在极点上产生接缝或夹点。下面的代码会将“UV”转换为笛卡尔坐标。

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 值, 我们可以得到:

../../_images/planet_normals.png

现在我们可以计算出球体表面的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);
../../_images/planet_noise.png

备注

为了突出显示纹理, 我们将材质设置为无阴影.

你现在可以看到, 尽管这看起来完全不像所承诺的球体, 但噪音确实无缝地包裹着球体. 对此, 让我们进入一些更丰富多彩的东西.

着色这个星球

现在来制作行星的颜色. 虽然有很多方法可以做到这一点, 但目前, 我们将使用水和陆地之间的梯度.

要在 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-11 之间平滑变化. 所以我们把它映射到 mix 预期的 0-1 范围内. 现在你可以看到, 颜色在蓝色和红色之间变化.

../../_images/planet_noise_color.png

这比我们想要的还要模糊一些. 行星通常在陆地和海洋之间有一个相对清晰的分隔. 为了做到这一点, 我们将把最后一项改为 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 , 如果第三个数字在第一个和第二个之间, 则在 01 之间平滑地混合. 所以在这一行中, 当 n 小于 -0.1 时, smoothstep 返回 0 , 当 n 高于 0 时, 它返回 1 .

../../_images/planet_noise_smooth.png

还有一件事, 使其更像一个行星. 这片土地不应该是圆球状的;让我们把边缘变得更粗糙一些. 在着色器中经常使用的一个技巧是在不同的频率下将不同层次的噪声叠加在一起, 使地形看起来粗糙. 我们使用一个层来制作大陆的整体球状结构. 然后, 另一层将边缘打碎, 然后是另一层, 以此类推. 我们要做的是用四行着色器代码来计算 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;

现在这个星球看起来像:

../../_images/planet_noise_fbm.png

而在重新打开阴影后, 看起来就像:

../../_images/planet_noise_fbm_shaded.png

制作海洋

让这个看起来更像是一颗行星的最后一件事. 海洋和陆地以不同的方式反射光线. 因此, 我们希望海洋比陆地更加闪耀. 我们可以通过将第四个值传递到输出 COLORalpha 通道并将其用作粗糙度图来实现.

COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);

该行对于水返回 0.3 , 对于土地返回 1.0 . 这意味着土地将变得很粗糙, 而水将变得非常光滑.

然后,在材质中,在“Metallic”(金属性)部分,请确保 Metallic0Specular1。这样做的原因是水对光线的反射非常好,但它不是金属的。这些值在物理上并不准确,但对于这个演示来说已经足够好了。

接下来, 在 "Roughness" 部分, 将 Roughness 设置为 1 , 并将粗糙度纹理设置为 Viewport Texture , 指向我们的行星纹理 Viewport . 最后, 将 Texture Channel 设置为 Alpha . 这将指示渲染器使用我们输出的 COLORalpha 通道作为 Roughness 值.

../../_images/planet_ocean.png

你会注意到, 除了行星不再反射天空外, 几乎没有什么变化. 这是因为默认情况下, 当某样东西以Alpha值渲染时, 它会被绘制为背景上的透明物体. 因为 Viewport 的默认背景是不透明的, 所以 Viewport Texturealpha 通道是 1 , 导致行星纹理的颜色略微变淡, 并且到处的 Roughness 值都是 1 . 为了纠正这个问题, 我们转到 Viewport 并启用 "Transparent Bg" 属性. 由于现在是在另一个透明物体上渲染, 要启用 blend_premul_alpha :

render_mode blend_premul_alpha;

这是将颜色预先乘以 alpha 值, 然后将它们正确地混合在一起. 通常情况下, 当在一个透明的颜色上混合另一个颜色时, 即使背景的 alpha0 (如本例), 也会出现奇怪的颜色渗漏问题. 设置 blend_premul_alpha 可以解决这个问题.

Now the planet should look like it is reflecting light on the ocean but not the land. If you haven't done so already, add an OmniLight3D to the scene so you can move it around and see the effect of the reflections on the ocean.

../../_images/planet_ocean_reflect.png

这就是你的作品。使用 Viewport 生成的程序式行星。