Tips
Are Certain Features Supported?
This list are for items that don’t already have dedicated pages in the documentation.
Feature |
Status |
---|---|
Destructibility |
Real-time modification is possible by changing the data and updating the maps and collision. You can sculpt heights, change textures, or make holes. If you want tunnels or caves though you need to add your own meshes or use Zylann’s Voxel Terrain. |
GPU Sculpting |
Pending. Currently painting occurs on the CPU in C++. It’s reasonably fast, but we have a soft limit of 200 on the brush size, as larger sizes lag. |
Holes |
Holes work for both visual and collision. |
Jolt |
Godot-Jolt works as a drop-in replacement for Godot Physics. Collision is generated where regions are defined. |
Non-destructive layers |
Used for things like river beds, roads or paths that follow a curve and tweak the terrain. It’s possible in the future. |
Object placement |
The instancer supports placing foliage. Placing objects that shouldn’t be in a MultiMeshInstance node is out of scope. See 3rd party tools below. |
Streaming |
Region Streaming is in progress. |
Water |
Use WaterWays for rivers, or Realistic Water Shader or Infinite Ocean for lakes or oceans. |
Rendering |
|
Frustum Culling |
The terrain is made up of several meshes, so half can be culled if the camera is near the ground. |
SDFGI |
Works fine. |
VoxelGI |
Works fine. |
Lightmaps |
Not possible. There is no static mesh, nor UV2 channel to bake lightmaps on to. |
3rd Party Tools |
|
For placing MeshInstance3D objects algorithmically, with or without collision. We provide a script that allows Scatter to detect our terrain. Or you can change collision mode to |
|
A level design tool for placing MeshInstance3D assets manually. Works on Terrain3D with placement mode set to Terrain3D or using the default mode and collision mode set to |
Regions
Outside of regions, there is no collision. Raycasts won’t hit anything. Querying terrain heights or other data will result in NANs or INF. Look through the API for specific return values. See [Collision] for more.
You can determine if a given location is within a region by using Terrain3DData.has_regionp(global_position)
. It will return -1 if the XZ location is not within a region. Y is ignored.
Terrain3DObjects
Just as the instancer keeps foliage stuck to the ground when sculpting, we provide a special node that does the same for regular MeshInstance3D objects.
Objects that are children of this node will maintain the same vertical offset relative to the terrain as they are moved laterally or as the terrain is sculpted.
For example you can place a sphere on the ground, move it laterally where a hill exists, and it will snap up to the new ground height. Or you can lower the ground and the sphere will drop with the changes.
You can then adjust the vertical position of the sphere so it is half embedded in the ground, then repeat either of the above and the sphere will snap with the same vertical offset, half embedded in the ground.
To use it:
Create a new node of type Terrain3DObjects
Add your MeshInstance3D objects as children of this node
Performance
The Terrain3DMaterial shader has some advanced features that look nice but consume some performance. You can get better performance by disabling them:
Set
WorldBackground
toFlat
orNone
Disable
Auto Shader
Disable
Dual Scaling
WorldBackground
asNoise
exposes additional shader settings, such as octaves and LOD. You can adjust these settings for performance. However this world generating noise is expensive. Consider not using it at all in a commercial game, and instead obscure your background with meshes, or use an HDR skybox with mountains built in.Reduce the size of the mesh and levels of detail by reducing
Mesh/Size
(mesh_size
) orMesh/Lods
(mesh_lods
) in theTerrain3D
node.Don’t use
Renderer/Cull Margin
. It should only be needed if using the noise background. Otherwise the AABB should be correctly calculated via editing, so there is no need to expand the cull margin. Keeping it enabled can cost more processing time.
Shaders
Minimal Shaders
This terrain is driven by the GPU, and controlled by our shader. We provide a minimal shader that has only the code needed to shape the terrain mesh without any texturing that you can use as a base to build your own. There’s also versions that use the color map, and have a low-poly look with flat normals. Find them all in extras/shaders/minimum.gdshader
.
Load this shader into the override shader slot and enable it. It includes no texturing so you can create your own.
Low-poly & PS1 Styles
Older style asthetics has a few different looks:
PS1 style often has blocky textures, which can be achieved by:
Use low res textures
In the Terrain3DTextureAsset, decreasing UV Scale
In the material, change Texture Filtering to Nearest. If you make your own shader, make sure to change your samplers to use Nearest filtering instead of Linear.
Low-poly Style often has large, flat shaded polygons. To get the best results:
Increase
vertex_spacing
to a large value like 10Start with a low-poly shader in
extras/shaders
Extend the shaders to make it your own. The low poly shaders don’t come with texturing, but you can combine the flat normal technique with the default textured shader if you want elements of both styles.
Day/Night cycles & light under the terrain
The terrain shader is set to cull_back
, meaning back faces are neither rendered, nor do they block light. If you have a day/night cycle and the sun sets below the horizon, it will shine through the terrain. Enable the shader override and change the second line to cull_disabled
and the horizon will block sunlight. This does cost performance.
Alternatively, turn off your light when it syncs below. This will likely cause a shader recompile in the engine as it’s a different lighting configuration. Instead you can change light energy to 0, or in the same frame, simultaneously turn off the sun light and turn on a moon light.
Accessing private shader variables
Variables in the shader prefaced with _
are considered private and not saved to disk. They can be accessed externally via the Rendering Server:
RenderingServer.material_get_param(terrain.get_material().get_material_rid(), "_background_mode")
Using the generated height map in other shaders
Here we get the resource ID of a material on a mesh. We assign the RID of the generated heightmap to texture slot in that material.
var mat: RID = $MeshInstance3D.mesh.surface_get_material(0).get_rid()
RenderingServer.material_set_param(mat, "texture_albedo", get_data().get_height_maps_rid())
This is a quick demonstration that shows results. However the generated texture arrays should be accessed with sampler2DArray in a shader, not the regular sampler which is what will will happen here.
This also works with the control and color maps.
Add a custom texture map
Here’s an example of using a custom texture map for one texture, such as adding an emissive texture for lava. Add in this code and add an emissive texture, then adjust the emissive ID to match the lava texture, and adjust the strength.
Add these uniforms at the top of the file with the other uniforms:
uniform int emissive_id : hint_range(0, 31) = 0;
uniform float emissive_strength = 1.0;
uniform sampler2D emissive_tex : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
Add a variable to store emissive value in the Material struct.
// struct Material {
...
vec3 emissive;
// };
Modify get_material()
to read the emissive texture with the next several options.
Add the initial value for emissive by adding a vec3 at the end
// void get_material(vec2 base_uv, ...
out_mat = Material(vec4(0.), vec4(0.), 0, 0, 0.0, vec3(0.));
Look for this conditional:
if (out_mat.blend > 0.) {
Right before that, add:
vec4 emissive = vec4(0.);
if(out_mat.base == emissive_id) {
emissive = textureGrad(emissive_tex, matUV, dd1.xy, dd1.zw);
}
// if (out_mat.blend > 0.) {
At the end of that block, before the }
, add:
vec4 emissive2 = vec4(0.);
emissive2 = textureGrad(emissive_tex, matUV2, dd2.xy, dd2.zw) * float(out_mat.over == emissive_id);
emissive = height_blend(emissive, albedo_ht.a, emissive2, albedo_ht2.a, out_mat.blend);
// }
At the end of the get_material()
function, add the emissive value to the material
// out_mat.alb_ht = albedo_ht;
// out_mat.nrm_rg = normal_rg;
out_mat.emissive = emissive.rgb;
// return;
// }
At the very bottom of fragment()
, before the final }
, apply the weighting and send it to the GPU.
vec3 emissive =
mat[0].emissive * weights.x +
mat[1].emissive * weights.y +
mat[2].emissive * weights.z +
mat[3].emissive * weights.w ;
EMISSION = emissive * emissive_strength;
// }
Next, add your emissive texture to the texture sampler and adjust the values on the newly exposed uniforms.
Avoid sub branches
Avoid placing an if statement within an if statement. Enable your FPS counter so you can test as you build your code. Some branch configurations may be free, some may be very expensive, or even more performant than you expect. Always test.
Sometimes it’s faster to always calculate than it is to branch.
Sometimes you can do tricks like this to avoid sub branching:
uniform bool auto_shader;
if (height > 256) {
if (auto_shader) {
albedo = snow_color;
}
}
uniform bool auto_shader;
if (height > 256) {
albedo = float(!auto_shader)*albedo + float(auto_shader)*snow_color;
}
These two are equivalent, and avoids the sub branch by always calculating. If auto_shader is true, the line is albedo = 0.*albedo + 1.*snow_color
.