Using a Viewport as a texture

Introduction

This tutorial will introduce you to using the Viewport 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

Note

This tutorial does not cover how to code a dynamic atmosphere like the one this planet has.

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.

In this tutorial, we'll cover the following topics:

  • How to use a Viewport as a render texture

  • Mapping a texture to a sphere with equirectangular mapping

  • Fragment shader techniques for procedural planets

  • Setting a Roughness map from a Viewport Texture

Setting up the Viewport

First, add a Viewport to the scene.

Next, set the size of the Viewport to (1024, 512). The Viewport 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.

../../_images/planet_new_viewport.png

Next, disable HDR and disable 3D. We don't need HDR because our planet's surface will not be especially bright, so values between 0 and 1 will be fine. And we will be using a ColorRect to render the surface, so we don't need 3D either.

Select the Viewport and add a ColorRect as a child.

Set the anchors "Right" and "Bottom" to 1, then make sure all the margins are set to 0. This will ensure that the ColorRect takes up the entire Viewport.

../../_images/planet_new_colorrect.png

Next, we add a Shader Material to the ColorRect (ColorRect > CanvasItem > Material > Material > New ShaderMaterial).

Note

Basic familiarity with shading is recommended for this tutorial. However, even if you are new to shaders, all the code will be provided, so you should have no problem following along.

ColorRect > CanvasItem > Material > Material > click / Edit > ShaderMaterial > Shader > New Shader > click / Edit:

shader_type canvas_item;

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

The above code renders a gradient like the one below.

../../_images/planet_gradient.png

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

Applying the texture

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

Then, from the menu that pops up, select the Viewport that we rendered to earlier.

../../_images/planet_pick_viewport_texture.png

Your sphere should now be colored in with the colors we rendered to the Viewport.

../../_images/planet_seam.png

Notice the ugly seam that forms where the texture wraps around? This is because we are picking a color based on UV coordinates and UV coordinates do not wrap around the texture. This is a classic problem in 2D map projection. Game developers often have a 2-dimensional map they want to project onto a sphere, but when it wraps around, it has large seams. There is an elegant workaround for this problem that we will illustrate in the next section.

Making the planet texture

So now, when we render to our Viewport, it appears magically on the sphere. But there is an ugly seam created by our texture coordinates. So how do we get a range of coordinates that wrap around the sphere in a nice way? One solution is to use a function that repeats on the domain of our texture. sin and cos are two such functions. Let's apply them to the texture and see what happens.

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.

Note

If you are interested in a little extra information on the technique, we will be converting from spherical coordinates into Cartesian coordinates. Spherical coordinates map the longitude and latitude of the sphere, while Cartesian coordinates are, for all intents and purposes, a vector from the center of the sphere to the point.

For each pixel, we will calculate its 3D position on the sphere. From that, we will use 3D noise to determine a color value. By calculating the noise in 3D, we solve the problem of the pinching at the poles. To understand why, picture the noise being calculated across the surface of the sphere instead of across the 2D plane. When you calculate across the surface of the sphere, you never hit an edge, and hence you never create a seam or a pinch point on the pole. The following code converts the UVs into Cartesian coordinates.

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(