使用 SubViewport 作为纹理

前言

This tutorial will introduce you to using the SubViewport as a texture that can be applied to 3D objects. In order to do so, it will walk you through the process of making a procedural planet like the one below:

../../_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 SubViewport to dynamically create textures that can be applied to the mesh.

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

  • How to use a SubViewport as a render texture

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

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

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

设置场景

Create a new scene and add the following nodes exactly as shown below.

../../_images/viewport_texture_node_tree.webp

Go into the the MeshInstance3D and make the mesh a SphereMesh

设置 SubViewport

Click on the SubViewport node and set its size to (1024, 512). The SubViewport can actually be any size so long as the width is double the height. The width needs to be double the height so that the image will accurately map onto the sphere, as we will be using equirectangular projection, but more on that later.

Next disable 3D. We will be using a ColorRect to render the surface, so we don’t need 3D either.

../../_images/planet_new_viewport.webp

Select the ColorRect and in the inspector set the anchors preset to Full Rect. This will ensure that the ColorRect takes up the entire SubViewport.

../../_images/planet_new_colorrect.webp

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

备注

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

Click the dropdown menu button for the shader material and click / Edit. From here go to Shader > New Shader. give it a name and click “Create”. click the shader in the inspector to open the shader editor. Delete the default code and add the following:

  1. shader_type canvas_item;
  2. void fragment() {
  3. COLOR = vec4(UV.x, UV.y, 0.5, 1.0);
  4. }

save the shader code, you’ll see in the inspector that the above code renders a gradient like the one below.

../../_images/planet_gradient.png

Now we have the basics of a SubViewport that we render to and we have a unique image that we can apply to the sphere.

应用纹理

Now 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 > 新建 StandardMaterial3D

然后点开 StandardMaterial3D 的下拉菜单,点击“编辑”

Go to the “Resource” section and check the Local to scene box. Then, go 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.webp

在检查器中点击刚才创建的 ViewportTexture,然后点击“分配”。接下来,在弹出的菜单中选择之前用于渲染的 Viewport。

../../_images/planet_pick_viewport_texture.webp

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

../../_images/planet_seam.webp

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

制作行星纹理

那么现在我们往 SubViewport 里渲染的东西就会神奇地出现在球体上。不过由于纹理坐标的原因,球体上会存在一条很丑的缝隙,我们该如何让坐标优雅地环绕球体呢?一种解决方法是使用在纹理域内重复的函数,比如 sincos。让我们把它们应用到纹理上,看看会发生什么。请将着色器中的已有颜色代码替换成下面的内容:

  1. COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);

../../_images/planet_sincos.webp

还凑合吧。现在球体的四周就再也看不到缝隙了,不过取而代之的是两个极点的地方会有收缩的现象。这种收缩的现象是 Godot 使用 StandardMaterial3D 将纹理映射到球体表面的方式造成的。这里使用的是一种叫做“等距柱状投影”的将球面图形转化为 2D 平面的技术。

备注

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

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

  1. float theta = UV.y * 3.14159;
  2. float phi = UV.x * 3.14159 * 2.0;
  3. vec3 unit = vec3(0.0, 0.0, 0.0);
  4. unit.x = sin(phi) * sin(theta);
  5. unit.y = cos(theta) * -1.0;
  6. unit.z = cos(phi) * sin(theta);
  7. unit = normalize(unit);

如果我们使用 unit 作为输出 COLOR 值, 我们可以得到:

../../_images/planet_normals.webp

现在我们可以计算出球体表面的3D位置, 可以使用3D噪声来制作球体. 直接从 Shadertoy 中使用这个噪声函数:

  1. vec3 hash(vec3 p) {
  2. p = vec3(dot(p, vec3(127.1, 311.7, 74.7)),
  3. dot(p, vec3(269.5, 183.3, 246.1)),
  4. dot(p, vec3(113.5, 271.9, 124.6)));
  5. return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
  6. }
  7. float noise(vec3 p) {
  8. vec3 i = floor(p);
  9. vec3 f = fract(p);
  10. vec3 u = f * f * (3.0 - 2.0 * f);
  11. return mix(mix(mix(dot(hash(i + vec3(0.0, 0.0, 0.0)), f - vec3(0.0, 0.0, 0.0)),
  12. dot(hash(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0)), u.x),
  13. mix(dot(hash(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0)),
  14. dot(hash(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0)), u.x), u.y),
  15. mix(mix(dot(hash(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0)),
  16. dot(hash(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0)), u.x),
  17. mix(dot(hash(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0)),
  18. dot(hash(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z );
  19. }

备注

所有功劳归作者Inigo Quilez所有. 它是在 MIT 许可下发布的.

现在使用 noised , 将以下内容添加到 fragment 函数中:

  1. float n = noise(unit * 5.0);
  2. COLOR.xyz = vec3(n * 0.5 + 0.5);

../../_images/planet_noise.webp

备注

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

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

着色这个星球

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

要在 GLSL 中创建渐变, 我们使用 mix 函数. mix 需要两个值来插值和第三个参数来选择在它们之间插入多少, 实质上它将两个值 混合 在一起. 在其他API中, 此函数通常称为 lerp . 虽然 lerp 通常用于将两个浮点数混合在一起, 但 mix 可以取任何值, 无论它是浮点数还是向量类型.

  1. 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.webp

这比我们想要的还要模糊一些. 行星通常在陆地和海洋之间有一个相对清晰的分隔. 为了做到这一点, 我们将把最后一项改为 smoothstep(-0.1, 0.0, n) . 整条线就变成了这样:

  1. 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.webp

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

  1. float n = noise(unit * 5.0) * 0.5;
  2. n += noise(unit * 10.0) * 0.25;
  3. n += noise(unit * 20.0) * 0.125;
  4. n += noise(unit * 40.0) * 0.0625;

现在这个星球看起来像:

../../_images/planet_noise_fbm.webp

制作海洋

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

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

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

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

Next, under the “Roughness” section set the roughness texture to a Viewport Texture pointing to our planet texture SubViewport. Finally, set the Texture Channel to Alpha. This instructs the renderer to use the alpha channel of our output COLOR as the Roughness value.

../../_images/planet_ocean.webp

You’ll notice that very little changes except that the planet is no longer reflecting the sky. This is happening because, by default, when something is rendered with an alpha value, it gets drawn as a transparent object over the background. And since the default background of the SubViewport is opaque, the alpha channel of the Viewport Texture is 1, resulting in the planet texture being drawn with slightly fainter colors and a Roughness value of 1 everywhere. To correct this, we go into the SubViewport and enable the “Transparent Bg” property. Since we are now rendering one transparent object on top of another, we want to enable blend_premul_alpha:

  1. 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. move around the OmniLight3D in the scene so you can see the effect of the reflections on the ocean.

../../_images/planet_ocean_reflect.webp

And there you have it. A procedural planet generated using a SubViewport.