Using a Viewport as a texture
Introduction
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: Note This tutorial does not cover how to code a dynamic atmosphere like the one this planet has. Camera, a light source, a Mesh Instance with a Primitive Mesh, and applying a SpatialMaterial 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:
- Viewport as a render texture
- Mapping a texture to a sphere with equirectangular mapping
- Fragment shader techniques for procedural planets
- Viewport Texture
Setting up the Viewport
Viewport to the scene. 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.0and1will be fine. And we will be using a ColorRect to render the surface, so we don’t need 3D either. ColorRect as a child.1, then make sure all the margins are set to0. This will ensure that the ColorRect takes up the entire Viewport. 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.New Shader> click / Edit:
The above code renders a gradient like the one below. Viewport that we render to and we have a unique image that we can apply to the sphere.shader_type canvas_item;void fragment() { COLOR = vec4(UV.x, UV.y, 0.5, 1.0);}
Applying the texture
New SpatialMaterial: Mesh Instance and add a SpatialMaterial 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).click/Edit: SpatialMaterial 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” Then, from the menu that pops up, select the Viewport that we rendered to earlier. Your sphere should now be colored in with the colors we rendered to the Viewport. 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
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.sinandcosare two such functions. Let’s apply them to the texture and see what happens.
SpatialMaterial. 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.COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
UVsinto 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(theta);unit = normalize(unit);
unitas an outputCOLORvalue, we get: Shadertoy:
Notevec3 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 );}
MITlicence.noise, add the following to thefragmentfunction:
Note In order to highlight the texture, we set the material to unshaded. You can see now that the noise indeed wraps seamlessly around the sphere. Although this looks nothing like the planet you were promised. So let’s move onto something more colorful.float n = noise(unit * 5.0);COLOR.xyz = vec3(n * 0.5 + 0.5);
Coloring the planet
Now to make the planet colors. While there are many ways to do this, for now, we will stick with a gradient between water and land.mixfunction.mixtakes two values to interpolate between and a third argument to choose how much to interpolate between them; in essence, it mixes the two values together. In other APIs, this function is often calledlerp. However,lerpis typically reserved for mixing two floats together;mixcan take any values whether it be floats or vector types.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.nsmoothly varies between-1and1. So we map it into the0-1range thatmixexpects. Now you can see that the colors change between blue and red.smoothstep(-0.1, 0.0, n). And thus the whole line becomes:COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), smoothstep(-0.1, 0.0, n));
smoothstepdoes is return0if the third argument is below the first and1if the third argument is larger than the second and smoothly blends between0and1if the third number is between the first and the second. So in this line,smoothstepreturns0whenevernis less than-0.1and it returns1whenevernis above0.nwith four lines of shader code instead of just one.nbecomes:
And now the planet looks like: And with shading turned back on, it looks like: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;
Making an ocean
alphachannel of our outputCOLORand using it as a Roughness map.COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);
0.3for water and1.0for land. This means that the land is going to be quite rough, while the water will be quite smooth.Metallicis set to0andSpecularis set to1. The reason for this is the water reflects light really well, but isn’t metallic. These values are not physically accurate, but they are good enough for this demo.Roughnessto1and set the roughness texture to a Viewport Texture pointing to our planet texture Viewport. Finally, set theTexture ChanneltoAlpha. This instructs the renderer to use thealphachannel of our outputCOLORas theRoughnessvalue. Viewport is opaque, thealphachannel of the Viewport Texture is1, resulting in the planet texture being drawn with slightly fainter colors and aRoughnessvalue of1everywhere. To correct this, we go into the Viewport and enable the “Transparent Bg” property. Since we are now rendering one transparent object on top of another, we want to enableblend_premul_alpha:render_mode blend_premul_alpha;
alphavalue and then blends them correctly together. Typically, when blending one transparent color on top of another, even if the background has analphaof0(as it does in this case), you end up with weird color bleed issues. Settingblend_premul_alphafixes that. OmniLight to the scene so you can move it around and see the effect of the reflections on the ocean. Viewport.
