3d-essentials

Use when working with 3D-specific systems — materials, lighting, shadows, environment, global illumination, fog, LOD, occlusion culling, and decals in Godot 4.3+

3D Essentials in Godot 4.3+

All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.

Related skills: player-controller for CharacterBody3D movement, physics-system for 3D collision shapes and raycasting, camera-system for Camera3D follow and transitions, shader-basics for spatial shaders and post-processing, godot-optimization for 3D performance tuning, animation-system for AnimationTree and 3D animation blending.


1. 3D Coordinate System & Core Nodes

Coordinate System

Godot uses a right-handed coordinate system with metric units (1 unit = 1 meter):

AxisDirectionColor
XRightRed
YUpGreen
ZOut of screen (+Z toward viewer)Blue

Cameras and lights point along -Z by default. When a character "faces forward," they look along -Z.

Essential 3D Nodes

NodePurpose
Node3DBase transform node — position, rotation, scale
MeshInstance3DDisplays a mesh with a material
Camera3DRequired to render 3D — perspective or orthogonal
DirectionalLight3DSun/moon — parallel rays, cheapest light
OmniLight3DPoint light — emits in all directions
SpotLight3DCone light — flashlights, spotlights
WorldEnvironmentSky, fog, tonemap, post-processing
DecalProjected texture onto surfaces
GPUParticles3DGPU-driven particle effects
CSGBox3D etc.Constructive Solid Geometry — prototyping
GridMap3D tile-based level building

Minimal 3D Scene

World (Node3D)
├── Camera3D
├── DirectionalLight3D
├── WorldEnvironment
├── MeshInstance3D (floor)
└── MeshInstance3D (player model)

2. Materials

StandardMaterial3D vs ShaderMaterial

MaterialUse ForNotes
StandardMaterial3DMost 3D objects — PBR workflowNo code; Inspector-driven
ORMMaterial3DSame as Standard but with packed ORM textureOcclusion+Roughness+Metallic in one texture
ShaderMaterialCustom effects — toon, water, dissolveRequires spatial shader code

Key StandardMaterial3D Properties

PropertyTypeDescription
albedo_colorColorBase surface color
albedo_textureTexture2DDiffuse/albedo texture map
metallicfloat0.0 (dielectric) to 1.0 (metal)
roughnessfloat0.0 (mirror) to 1.0 (matte)
emissionColorSelf-illumination color
emission_energy_multiplierfloatEmission brightness
normal_mapTexture2DSurface detail without extra geometry
ao_textureTexture2DAmbient occlusion map
heightmap_textureTexture2DRay-marched parallax depth illusion
rimfloatEdge lighting (micro-fur scattering)
clearcoatfloatTransparent secondary coat (car paint)

Transparency Modes

ModePerformanceShadowsUse For
DisabledFastestYesFully opaque objects
AlphaSlowNoSemi-transparent glass, water
Alpha ScissorFastYesBinary cutout (leaves, fences)
Alpha HashMediumYesDithered transparency (hair)
Depth Pre-PassMediumPartialMostly opaque with transparent edges

Setting Materials from Code

GDScript

@onready var mesh: MeshInstance3D = $MeshInstance3D

func _ready() -> void:
    var mat := StandardMaterial3D.new()
    mat.albedo_color = Color(0.8, 0.2, 0.2)
    mat.metallic = 0.3
    mat.roughness = 0.7
    mesh.material_override = mat

func flash_emissive() -> void:
    var mat: StandardMaterial3D = mesh.material_override
    mat.emission_enabled = true
    mat.emission = Color.WHITE
    mat.emission_energy_multiplier = 3.0
    var tween := create_tween()
    tween.tween_property(mat, "emission_energy_multiplier", 0.0, 0.3)
    tween.tween_callback(func(): mat.emission_enabled = false)

C#

private MeshInstance3D _mesh;

public override void _Ready()
{
    _mesh = GetNode<MeshInstance3D>("MeshInstance3D");
    var mat = new StandardMaterial3D();
    mat.AlbedoColor = new Color(0.8f, 0.2f, 0.2f);
    mat.Metallic = 0.3f;
    mat.Roughness = 0.7f;
    _mesh.MaterialOverride = mat;
}

public void FlashEmissive()
{
    var mat = _mesh.MaterialOverride as StandardMaterial3D;
    mat.EmissionEnabled = true;
    mat.Emission = Colors.White;
    mat.EmissionEnergyMultiplier = 3.0f;
    var tween = CreateTween();
    tween.TweenProperty(mat, "emission_energy_multiplier", 0.0f, 0.3);
    tween.TweenCallback(Callable.From(() => mat.EmissionEnabled = false));
}

Material Instancing

When multiple MeshInstance3D nodes share the same material, changing one affects all. To make a per-instance copy:

# In _ready() — creates an independent copy of the material
mesh.material_override = mesh.material_override.duplicate()
_mesh.MaterialOverride = (Material)_mesh.MaterialOverride.Duplicate();

3. Lighting

Light Types Comparison

LightShapeShadowsCostMax Visible
DirectionalLight3DParallel raysPSSMCheapest8 (Forward+)
OmniLight3DSphereCube/Dual ParaboloidMedium512 clustered*
SpotLight3DConeSingle textureCheap512 clustered*

*Forward+ shares 512 clustered element slots among omni lights, spot lights, decals, and reflection probes.

Light Properties

GDScript

@onready var sun: DirectionalLight3D = $DirectionalLight3D

func _ready() -> void:
    sun.light_color = Color(1.0, 0.95, 0.9)
    sun.light_energy = 1.0
    sun.shadow_enabled = true

    # Directional shadow quality
    sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS
    sun.directional_shadow_max_distance = 100.0

C#

private DirectionalLight3D _sun;

public override void _Ready()
{
    _sun = GetNode<DirectionalLight3D>("DirectionalLight3D");
    _sun.LightColor = new Color(1.0f, 0.95f, 0.9f);
    _sun.LightEnergy = 1.0f;
    _sun.ShadowEnabled = true;

    _sun.DirectionalShadowMode = DirectionalLight3D.ShadowMode.Parallel4Splits;
    _sun.DirectionalShadowMaxDistance = 100.0f;
}

Dynamic Point Light

func create_explosion_light(pos: Vector3) -> void:
    var light := OmniLight3D.new()
    light.light_color = Color(1.0, 0.6, 0.2)
    light.light_energy = 4.0
    light.omni_range = 10.0
    light.omni_attenuation = 2.0
    light.position = pos
    add_child(light)

    var tween := create_tween()
    tween.tween_property(light, "light_energy", 0.0, 0.5)
    tween.tween_callback(light.queue_free)
public void CreateExplosionLight(Vector3 pos)
{
    var light = new OmniLight3D();
    light.LightColor = new Color(1.0f, 0.6f, 0.2f);
    light.LightEnergy = 4.0f;
    light.OmniRange = 10.0f;
    light.OmniAttenuation = 2.0f;
    light.Position = pos;
    AddChild(light);

    var tween = CreateTween();
    tween.TweenProperty(light, "light_energy", 0.0f, 0.5);
    tween.TweenCallback(Callable.From(light.QueueFree));
}

Shadow Configuration Tips

SettingEffectRecommendation
shadow_biasPrevents self-shadowing (shadow acne)Start at 0.1, increase if acne visible
shadow_normal_biasBetter acne fix than regular biasPrefer this over shadow_bias
directional_shadow_max_distanceLimits shadow range from cameraLower = better quality; 50–100m typical
Shadow map resolutionProject Settings > Rendering > Lights and Shadows2048 for perf, 4096 for quality
shadow_blurSoftens shadow edges1.0–2.0 for gentle softness

Light Bake Modes

ModeDescriptionUse For
DisabledNot included in lightmap baking; fully real-time (default)Moving lights, player flashlight
StaticFully baked into lightmaps — no runtime costArchitecture, terrain, fixed lights
DynamicIndirect light baked, direct light stays real-timeLights that change color/intensity

4. Environment & Post-Processing

WorldEnvironment Setup

World (Node3D)
├── WorldEnvironment     ← holds Environment + CameraAttributes resources
├── DirectionalLight3D
├── Camera3D
└── ...

Set the Environment resource on WorldEnvironment and the Camera Attributes for exposure/DOF.

Sky Options

Sky MaterialDescriptionUse For
PanoramaSkyMaterial360° HDR panorama imageRealistic environments
ProceduralSkyMaterialGenerated sky with color gradientsQuick prototyping
PhysicalSkyMaterialPhysics-based atmosphere + sunOutdoor day/night cycles

GDScript

func setup_environment() -> void:
    var env := Environment.new()

    # Sky
    var sky := Sky.new()
    var sky_mat := ProceduralSkyMaterial.new()
    sky_mat.sky_top_color = Color(0.4, 0.6, 1.0)
    sky_mat.sky_horizon_color = Color(0.7, 0.8, 1.0)
    sky_mat.ground_bottom_color = Color(0.2, 0.15, 0.1)
    sky.sky_material = sky_mat
    env.sky = sky
    env.background_mode = Environment.BG_SKY

    # Tonemap
    env.tonemap_mode = Environment.TONE_MAP_FILMIC
    env.tonemap_exposure = 1.0

    # Ambient light from sky
    env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY

    $WorldEnvironment.environment = env

C#

public void SetupEnvironment()
{
    var env = new Godot.Environment();

    var sky = new Sky();
    var skyMat = new ProceduralSkyMaterial();
    skyMat.SkyTopColor = new Color(0.4f, 0.6f, 1.0f);
    skyMat.SkyHorizonColor = new Color(0.7f, 0.8f, 1.0f);
    skyMat.GroundBottomColor = new Color(0.2f, 0.15f, 0.1f);
    sky.SkyMaterial = skyMat;
    env.Sky = sky;
    env.BackgroundMode = Godot.Environment.BGMode.Sky;

    env.TonemapMode = Godot.Environment.ToneMapper.Filmic;
    env.TonemapExposure = 1.0f;

    env.AmbientLightSource = Godot.Environment.AmbientSource.Sky;

    GetNode<WorldEnvironment>("WorldEnvironment").Environment = env;
}

Tonemap Modes

ModeCharacterBest For
LinearClips brights — blown-out lookDebug, deliberately flat look
ReinhardSimple curve, preserves brightsGeneral use
FilmicFilm-like contrastCinematic games
ACESHigh contrast with desaturationRealistic/photographic
AgXMaintains hue as brightness increasesPhysically accurate lighting

Post-Processing Effects (Inspector)

Configure these on the Environment resource — no shader code needed:

EffectDescriptionRenderer Support
GlowBloom/glow on bright surfacesAll
SSAOScreen-space ambient occlusionForward+ only
SSILScreen-space indirect lightingForward+ only
SSRScreen-space reflectionsForward+ only
SDFGIReal-time GI for large scenesForward+ only
DOFDepth of field blur (via CameraAttributes)All
FogDepth and height fogAll
AdjustmentsBrightness, contrast, saturation, color correctionAll
Auto ExposureAdaptive exposure (via CameraAttributes)Forward+, Mobile

5. Global Illumination

GI Methods Comparison

MethodQualityPerformanceDynamicRendererUse For
None (ambient)LowFreeYesAllSimple/stylized games
ReflectionProbeMediumLowOptionalAllLocalized reflections
LightmapGIHighFree at runtimeNoForward+Static scenes (archviz)
VoxelGIHighHighYesForward+Small-medium dynamic scenes
SDFGIMedium-HighMediumYesForward+Large open-world scenes

ReflectionProbe

Captures the surrounding environment into a cubemap for reflections on nearby objects.

Room (Node3D)
├── ReflectionProbe      ← extents cover the room
├── MeshInstance3D (walls)
└── MeshInstance3D (shiny floor)
@onready var probe: ReflectionProbe = $ReflectionProbe

func _ready() -> void:
    probe.size = Vector3(10.0, 4.0, 10.0)  # cover the room
    probe.update_mode = ReflectionProbe.UPDATE_ONCE  # bake once, free at runtime
public override void _Ready()
{
    var probe = GetNode<ReflectionProbe>("ReflectionProbe");
    probe.Size = new Vector3(10.0f, 4.0f, 10.0f);
    probe.UpdateMode = ReflectionProbe.UpdateModeEnum.Once;
}

LightmapGI (Baked)

Best quality, zero runtime cost. Requires UV2 on meshes (auto-generated on import).

  1. Add a LightmapGI node to the scene
  2. Set all static lights to Bake Mode: Static
  3. Set all static meshes to GI Mode: Static in the GeometryInstance3D section
  4. Select LightmapGI → click Bake Lightmaps in the toolbar
  5. Baked data is saved as a LightmapGIData resource — commit it with your project

SDFGI (Real-Time)

Enable on the Environment resource — no nodes needed:

var env: Environment = $WorldEnvironment.environment
env.sdfgi_enabled = true
env.sdfgi_cascades = 4
env.sdfgi_use_occlusion = true
var env = GetNode<WorldEnvironment>("WorldEnvironment").Environment;
env.SdfgiEnabled = true;
env.SdfgiCascades = 4;
env.SdfgiUseOcclusion = true;

6. Fog

Depth & Height Fog (Environment)

Simple fog configured on the Environment resource:

GDScript

var env: Environment = $WorldEnvironment.environment

# Depth fog — increases with distance from camera
env.fog_enabled = true
env.fog_light_color = Color(0.7, 0.75, 0.8)
env.fog_density = 0.01

# Height fog — thicker below a certain Y level
env.fog_height = 0.0
env.fog_height_density = 0.5

# Sun scattering — tints fog with directional light color
env.fog_sun_scatter = 0.3

C#

var env = GetNode<WorldEnvironment>("WorldEnvironment").Environment;
env.FogEnabled = true;
env.FogLightColor = new Color(0.7f, 0.75f, 0.8f);
env.FogDensity = 0.01f;
env.FogHeight = 0.0f;
env.FogHeightDensity = 0.5f;
env.FogSunScatter = 0.3f;

Volumetric Fog (Forward+ only)

Realistic fog that interacts with lights and casts volumetric shadows.

Enable on the Environment:

env.volumetric_fog_enabled = true
env.volumetric_fog_density = 0.05
env.volumetric_fog_albedo = Color(0.9, 0.9, 0.9)
env.volumetric_fog_emission = Color(0.0, 0.0, 0.0)
env.volumetric_fog_length = 64.0
env.volumetric_fog_temporal_reprojection_enabled = true
var env = GetNode<WorldEnvironment>("WorldEnvironment").Environment;
env.VolumetricFogEnabled = true;
env.VolumetricFogDensity = 0.05f;
env.VolumetricFogAlbedo = new Color(0.9f, 0.9f, 0.9f);
env.VolumetricFogEmission = new Color(0.0f, 0.0f, 0.0f);
env.VolumetricFogLength = 64.0f;
env.VolumetricFogTemporalReprojectionEnabled = true;

FogVolume (Localized Fog)

Create fog in specific areas (caves, steam vents, magic effects):

Scene
├── WorldEnvironment (volumetric_fog_enabled = true, density = 0.0)
└── FogVolume
    shape = Box
    size = Vector3(5, 3, 5)
    material = FogMaterial (density = 1.0)
func create_fog_cloud(pos: Vector3) -> void:
    var fog := FogVolume.new()
    fog.shape = RenderingServer.FOG_VOLUME_SHAPE_ELLIPSOID
    fog.size = Vector3(4.0, 2.0, 4.0)
    fog.position = pos

    var mat := FogMaterial.new()
    mat.density = 0.5
    mat.albedo = Color(0.8, 0.85, 0.9)
    fog.material = mat

    add_child(fog)
public void CreateFogCloud(Vector3 pos)
{
    var fog = new FogVolume();
    fog.Shape = RenderingServer.FogVolumeShape.Ellipsoid;
    fog.Size = new Vector3(4.0f, 2.0f, 4.0f);
    fog.Position = pos;

    var mat = new FogMaterial();
    mat.Density = 0.5f;
    mat.Albedo = new Color(0.8f, 0.85f, 0.9f);
    fog.Material = mat;

    AddChild(fog);
}

Set global volumetric_fog_density to 0.0 and use FogVolume nodes to place fog only where needed. This gives full control without blanket fog everywhere.


7. Decals

Decals project textures onto surfaces without modifying the underlying mesh. Use for bullet holes, blood splatters, footprints, ground markings.

Scene Setup

World
├── MeshInstance3D (floor)
└── Decal
    size = Vector3(1, 0.5, 1)   ← Y controls projection depth
    texture_albedo = footprint.png
    texture_normal = footprint_normal.png

Spawning Decals from Code

GDScript

func spawn_bullet_hole(hit_pos: Vector3, hit_normal: Vector3) -> void:
    var decal := Decal.new()
    decal.size = Vector3(0.3, 0.2, 0.3)
    decal.texture_albedo = preload("res://textures/bullet_hole.png")
    decal.position = hit_pos

    # Orient decal to project along the hit surface normal
    # Decals project along -Y, so rotate to align -Y with -hit_normal
    if hit_normal.abs() != Vector3.UP:
        decal.look_at(hit_pos - hit_normal, Vector3.UP)
        decal.rotate_object_local(Vector3.RIGHT, PI / 2.0)
    # For floor/ceiling hits (normal is UP or DOWN), default orientation works

    # Fade and cleanup
    decal.distance_fade_enabled = true
    decal.distance_fade_begin = 20.0
    decal.distance_fade_length = 5.0

    get_parent().add_child(decal)

    # Remove after 30 seconds
    get_tree().create_timer(30.0).timeout.connect(decal.queue_free)

C#

public void SpawnBulletHole(Vector3 hitPos, Vector3 hitNormal)
{
    var decal = new Decal();
    decal.Size = new Vector3(0.3f, 0.2f, 0.3f);
    decal.TextureAlbedo = GD.Load<Texture2D>("res://textures/bullet_hole.png");
    decal.Position = hitPos;

    if (hitNormal.Abs() != Vector3.Up)
    {
        decal.LookAt(hitPos - hitNormal, Vector3.Up);
        decal.RotateObjectLocal(Vector3.Right, Mathf.Pi / 2.0f);
    }

    decal.DistanceFadeEnabled = true;
    decal.DistanceFadeBegin = 20.0f;
    decal.DistanceFadeLength = 5.0f;

    GetParent().AddChild(decal);

    GetTree().CreateTimer(30.0f).Timeout += decal.QueueFree;
}

Decal Limits

RendererMax Decals
Forward+512 clustered elements (shared with lights, probes)
Mobile8 per mesh resource
CompatibilityLimited by max renderable elements

8. Optimization — LOD, Culling, MultiMesh

Mesh LOD (Automatic)

Godot auto-generates LOD levels on import for glTF, Blend, Collada, and FBX files. No manual setup needed.

Control LOD aggressiveness:

# Per-object LOD bias (GeometryInstance3D property)
# > 1.0 = keep high detail longer, < 1.0 = switch to low detail sooner
$MeshInstance3D.lod_bias = 1.5
GetNode<MeshInstance3D>("MeshInstance3D").LodBias = 1.5f;

Global LOD threshold: Project Settings > Rendering > Mesh LOD > LOD Change > Threshold Pixels (default 1.0 — perceptually lossless).

Visibility Ranges (Manual LOD)

For custom LOD with different meshes at different distances:

# Close-up: full-detail tree (0–30m)
$TreeDetailed.visibility_range_begin = 0.0
$TreeDetailed.visibility_range_end = 30.0
$TreeDetailed.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF

# Far: billboard impostor (25–100m, with crossfade overlap)
$TreeBillboard.visibility_range_begin = 25.0
$TreeBillboard.visibility_range_end = 100.0
$TreeBillboard.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF

Occlusion Culling

Prevents rendering objects hidden behind walls/large geometry.

Setup:

  1. Enable: Project Settings > Rendering > Occlusion Culling > Use Occlusion Culling
  2. Add an OccluderInstance3D node to your scene
  3. Select it → click Bake Occluders in the 3D toolbar
  4. Exclude dynamic objects via Bake > Cull Mask (assign them to different visual layers)

Only bake occluders from static geometry. Moving OccluderInstance3D nodes at runtime forces expensive BVH rebuilds.

MultiMeshInstance3D

Render thousands of identical meshes (grass, trees, debris) in a single draw call.

GDScript

func spawn_grass(positions: PackedVector3Array) -> void:
    var mm := MultiMesh.new()
    mm.transform_format = MultiMesh.TRANSFORM_3D
    mm.mesh = preload("res://meshes/grass_blade.tres")
    mm.instance_count = positions.size()

    for i in positions.size():
        var xform := Transform3D()
        xform.origin = positions[i]
        # Random rotation around Y
        xform = xform.rotated(Vector3.UP, randf() * TAU)
        # Random scale variation
        var s := randf_range(0.8, 1.2)
        xform = xform.scaled(Vector3(s, s, s))
        mm.set_instance_transform(i, xform)

    var mmi := MultiMeshInstance3D.new()
    mmi.multimesh = mm
    add_child(mmi)

C#

public void SpawnGrass(Vector3[] positions)
{
    var mm = new MultiMesh();
    mm.TransformFormat = MultiMesh.TransformFormatEnum.Transform3D;
    mm.Mesh = GD.Load<Mesh>("res://meshes/grass_blade.tres");
    mm.InstanceCount = positions.Length;

    for (int i = 0; i < positions.Length; i++)
    {
        var xform = Transform3D.Identity;
        xform.Origin = positions[i];
        xform = xform.Rotated(Vector3.Up, (float)GD.RandRange(0, Mathf.Tau));
        float s = (float)GD.RandRange(0.8, 1.2);
        xform = xform.Scaled(new Vector3(s, s, s));
        mm.SetInstanceTransform(i, xform);
    }

    var mmi = new MultiMeshInstance3D();
    mmi.Multimesh = mm;
    AddChild(mmi);
}

9. Renderer Comparison

FeatureForward+MobileCompatibility
SSAO / SSIL / SSRYesNoNo
Volumetric FogYesNoNo
SDFGIYesNoNo
LightmapGIYesYesYes
VoxelGIYesNoNo
Glow / BloomYesYesYes
Max Omni+Spot per mesh512 clustered8+88+8 (adjustable)
Target HardwareDesktop/ConsoleMobile/Mid-rangeLow-end/WebGL

Choose in Project Settings > Rendering > Renderer > Rendering Method.

Rule of thumb: Start with Forward+ for desktop. Switch to Mobile for mobile targets. Use Compatibility only for web or very low-end hardware.


10. Common Pitfalls

SymptomCauseFix
3D scene is completely blackNo Camera3D or no lights in sceneAdd Camera3D + DirectionalLight3D + WorldEnvironment
Objects appear dark despite lightingNo ambient light or skySet Environment ambient_light_source to Sky or Color
Shadow acne (striped shadows)Shadow bias too lowIncrease shadow_normal_bias (preferred over shadow_bias)
Peter-panning (shadows detached)Shadow bias too highLower shadow_bias; use shadow_normal_bias instead
Shadows pop in/outdirectional_shadow_max_distance too highLower to 50–100m; quality improves as range shrinks
Material looks flat / no reflectionsMissing ReflectionProbe or SkyAdd ReflectionProbe or set Environment reflected light to Sky
Decals don't appearY extent too small or wrong cull maskIncrease Decal Y size; check cull mask matches target layer
Transparency sorting artifactsOverlapping transparent meshesUse Alpha Scissor/Hash where possible; avoid layered transparency
SDFGI shows light leakingThin walls or small geometryThicken walls; increase SDFGI cascade count
Volumetric fog not visibleWrong renderer (Mobile/Compatibility)Switch to Forward+ renderer
MultiMesh instances invisibleinstance_count set after transformsSet instance_count before calling set_instance_transform()

11. Implementation Checklist

  • Scene has Camera3D, at least one light source, and WorldEnvironment
  • Environment has a sky material (procedural or HDR panorama) for ambient and reflected light
  • Tonemap mode is set (Filmic or ACES for realistic look, AgX for physically accurate)
  • DirectionalLight3D has shadows enabled with shadow_normal_bias tuned to prevent acne
  • directional_shadow_max_distance is set to the minimum needed (50–100m typical)
  • Static geometry uses StandardMaterial3D with appropriate PBR textures (albedo, normal, roughness, metallic)
  • Transparent materials use Alpha Scissor or Alpha Hash instead of Alpha where possible (performance + shadows)
  • ReflectionProbes are placed in rooms/areas with reflective surfaces
  • GI method chosen based on project needs (LightmapGI for static, SDFGI for large dynamic, VoxelGI for small dynamic)
  • Mesh LOD is enabled on import (default for glTF/Blend — verify OBJ files)
  • Occlusion culling is enabled and baked for scenes with heavy occlusion (indoor, urban)
  • MultiMeshInstance3D is used for instanced geometry (grass, trees, props) instead of individual nodes
  • Renderer matches target platform (Forward+ desktop, Mobile mobile, Compatibility web)