This guide teaches you how to create a 3D scene programmatically in Godot 4.3, complete with a custom skybox, using only code (no editor setup). We’ll explain core concepts like WorldEnvironment, Camera3D, and SkyMaterial.

Final Code Structure

Project/
├── scripts/
│   ├── main.gd       # Root scene script
│   └── skybox.gd     # Skybox configuration
└── textures/
    └── default_sky.hdr  # HDRI skybox texture

1. Setting Up the Main Scene (main.gd)

🔹 Code

extends Node3D

func _ready():
    # 1. Configure camera
    var camera = Camera3D.new()
    camera.position = Vector3(0, 1, 3)  # Position (X, Y, Z)
    camera.look_at(Vector3.ZERO)        # Look at world origin
    add_child(camera)
    camera.make_current()  # Activate this camera

    # 2. Add skybox
    var skybox = load("res://Scripts/skybox.gd").new()
    add_child(skybox)

🔹 Explanation

  1. Why We Need a Camera:

    • Godot renders 3D scenes only through a camera.
    • camera.make_current() ensures this is the active viewpoint.
  2. Camera Positioning:

    • Vector3(0, 1, 3) places the camera 3 units back and 1 unit up.
    • look_at(Vector3.ZERO) makes it point at the scene center.
  3. Adding the Skybox:

    • skybox.gd is loaded as a WorldEnvironment node.
    • Added to the scene tree to affect global rendering.

2. Creating the Skybox (skybox.gd)

🔹 Code

extends WorldEnvironment

func _init():
    # 1. Create Environment resource
    var env = Environment.new()
  
    # 2. Configure background as sky
    env.background_mode = Environment.BG_SKY
  
    # 3. Create sky material with HDRI texture
    var sky_material = PanoramaSkyMaterial.new()
    sky_material.panorama = load("res://Textures/default_sky.hdr")
  
    # 4. Assign material to Sky resource
    var sky = Sky.new()
    sky.sky_material = sky_material
    env.sky = sky
  
    # 5. Configure HDR exposure
    var camera_attrs = CameraAttributesPhysical.new()
    camera_attrs.exposure_multiplier = 1.5  # Brightness control
    env.camera_attributes = camera_attrs
  
    # 6. Apply settings to WorldEnvironment
    environment = env

🔹 Step-by-Step Explanation

  1. WorldEnvironment Node:

    • Controls global rendering settings (sky, fog, ambient light).
    • Only one can exist per scene.
  2. BG_SKY Mode:

    • Tells Godot to use a skybox instead of a solid color.
    • (Common Mistake: Forgetting this results in a blank background).
  3. PanoramaSkyMaterial:

    • Uses an equirectangular (360°) HDRI texture.
    • .hdr files provide realistic lighting data.
  4. Sky Resource:

    • Acts as a container for sky materials.
    • Required even though we’re using a panorama.
  5. CameraAttributesPhysical:

    • Adjusts exposure for HDR lighting.
    • Without this, your skybox might look too dark/bright.

3. Key Concepts

🔹 Why Use WorldEnvironment?

  • Global Settings: Affects the entire scene (not just specific objects).
  • Sky Rendering: Provides background and ambient lighting.
  • Post-Processing: Can add effects like fog or color grading.

🔹 HDRI Textures Explained

  • What They Are: 360° images that capture real-world lighting.
  • Why Use Them:
    • Realistic backgrounds.
    • Natural lighting for objects (no need for extra lights).

🔹 Camera Positioning Tips

  • look_at(): Ensures the camera faces the target point.
  • Z-Axis: Moving backward uses positive Z values in Godot’s 3D space.

4. Common Issues & Fixes

🔸 Sky Not Appearing

  1. Check background_mode is BG_SKY.
  2. Verify texture path in PanoramaSkyMaterial.
  3. Ensure exposure_multiplier isn’t too low (try 2.0).

🔸 Purple/Black Skybox

  • Texture Not Loaded: Confirm:
    • File exists at res://Textures/default_sky.hdr.
    • No typos in path (case-sensitive on Linux/macOS).

🔸 Objects Too Dark

  • Add ambient light in skybox.gd:
    env.ambient_light_color = Color(1, 1, 1)
    env.ambient_light_energy = 0.3
    

5. Understanding MeshInstance3D and Mesh (Beginner-Friendly Guide)

At this point, we’ve successfully set up:

  1. A camera so we can see the world.
  2. A skybox to provide a realistic background.

However, our scene still looks empty because there are no physical objects in it yet. To fix that, we’re going to add a cube.


🔹 5.1: What is a 3D Mesh?

Before we jump into the code, let’s break down a core concept: what a “mesh” is in 3D graphics.

What is a Mesh?

A mesh is the shape of a 3D object. Imagine a cube, a sphere, or a character model in a video game—all of these are made up of meshes.

In technical terms:

  • A mesh is a collection of vertices (points) connected by edges (lines) , forming faces (triangles or polygons) .
  • The GPU (Graphics Processing Unit) takes these points and renders them into a solid shape.

How Does Godot Handle Meshes?

Godot has prebuilt meshes that let us create 3D objects quickly, including:

  • BoxMesh → A cube.
  • SphereMesh → A ball.
  • CylinderMesh → A tube.

These meshes exist only as data —they don’t appear in the world unless we assign them to something.


🔹 5.2: What is a MeshInstance3D?

A MeshInstance3D is what actually makes a mesh visible in the 3D world.

Think of it like this:

  • A mesh is just raw data (like a blueprint).
  • A MeshInstance3D is a physical object that holds a mesh and makes it appear in the scene.

Analogy

If a mesh is like the blueprint for a house, then a MeshInstance3D is the actual house built from that blueprint.

A MeshInstance3D does two important things :

  1. It holds a mesh, which gives it shape.
  2. It exists inside the 3D world, so the camera can see it.

🔹 5.3: Adding a Cube to the Scene

Now that we understand meshes and MeshInstance3D, let’s add a cube to the game world.

📝 Updated main.gd Code

extends Node3D

var cube: MeshInstance3D  # We declare a cube variable (empty for now)

# Animate the cube's rotation every frame
func _process(delta):
    cube.rotate_y(delta * 0.5)  # This makes the cube spin

func _ready():
    # Create and configure the camera
    var camera = Camera3D.new()
    camera.position = Vector3(0, 1, 3)  # Moves the camera up and back
    camera.look_at(Vector3.ZERO)        # Makes it face the center of the world
    add_child(camera)
    camera.make_current()  # Set this as the active camera

    # Create a new cube instance
    cube = MeshInstance3D.new()  # This is a new 3D object
    cube.mesh = BoxMesh.new()    # We assign a cube mesh (BoxMesh)
    add_child(cube)              # Add the cube to the scene so it appears

    # Add world environment (Skybox)
    var skybox = load("res://Scripts/skybox.gd").new()
    add_child(skybox)

🔹 5.4: Breaking Down the Code

Let’s break this down step by step , making sure we understand every single line .

Step 1️⃣ – Creating a Camera

var camera = Camera3D.new()
camera.position = Vector3(0, 1, 3)
camera.look_at(Vector3.ZERO)
add_child(camera)
camera.make_current()
  • Camera3D.new() → Creates a new camera.
  • camera.position = Vector3(0, 1, 3) → Moves it up and back so we can see the scene.
  • camera.look_at(Vector3.ZERO) → Rotates the camera so it faces the center of the world .
  • add_child(camera) → Adds it to the scene so it actually exists.
  • camera.make_current() → Sets it as the active camera .

Step 2️⃣ – Creating a Cube

cube = MeshInstance3D.new()  # Creates a new MeshInstance3D object
cube.mesh = BoxMesh.new()    # Assigns a cube mesh
add_child(cube)              # Adds the cube to the scene

This does three things :

  1. MeshInstance3D.new() → Creates a new 3D object.
  2. BoxMesh.new() → Assigns it a cube shape.
  3. add_child(cube) → Adds the cube to the scene, making it visible .

Without add_child(cube), the cube would exist in memory but never appear .


Step 3️⃣ – Adding a Skybox

var skybox = load("res://Scripts/skybox.gd").new()
add_child(skybox)

This loads our skybox.gd script, which adds a background to the scene .


🔹 5.5: Making the Cube Rotate

Right now, the cube is static . Let’s make it rotate.

func _process(delta):
    cube.rotate_y(delta * 0.5)  # Spins the cube around the Y-axis

Breaking This Down

  • _process(delta) runs every frame in the game.
  • cube.rotate_y(angle) rotates the cube around the Y-axis .
  • delta ensures smooth rotation no matter the frame rate.

🔹 5.6: Running the Scene

If you run the game now , you should see:

  1. A skybox background 🌄
  2. A spinning cube in the center 🟦
  3. A camera showing the scene 🎥

🎉 Congratulations! You’ve created your first fully-coded 3D scene! 🚀


🔹 5.7: Troubleshooting & Common Issues

🔸 The Cube Doesn’t Appear

  1. Make sure the cube is added to the scene :
   print("Cube added:", cube)

If this doesn’t print anything, add_child(cube) might not be running.

  1. Check the camera position :
  • If the camera is inside the cube, try moving it further back:
    camera.position = Vector3(0, 2, 5)
    

🔸 The Cube Isn’t Rotating

  1. Check if _process(delta) is running :
   print("Rotation running...")

If this doesn’t print, Godot isn’t calling _process(). Try restarting the scene.

  1. Check for errors in the console :
  • If cube is null, something is wrong with how it’s being assigned.

Step 6: Importing and Displaying a Custom 3D Model in Godot 4.3

At this stage, we have successfully established a 3D scene that includes a camera, skybox, and a rotating cube. However, the cube is merely a placeholder for testing purposes. Now, we will replace the cube with a custom 3D model, specifically whichever glb file you made in the previous guides. If you did not, go back and review or download one online.


🔹 Step 6.1: Understanding GLB Files and 3D Models in Godot

Before integrating our 000_Snowpuff.glb model, it is essential to understand what a 3D model is and how Godot processes it.

What is a 3D Model?

A 3D model is a digital representation of an object in three-dimensional space, created using vertices (points) connected by edges and faces to form a shape.

These models can be created using 3D modeling software such as:

  • Blender
  • Autodesk Maya
  • 3ds Max
  • Cinema 4D

3D models can be textured, rigged (for animation), and include materials that define how they look in a scene.

What is a GLB File?

GLB (or GLTF Binary) is a universal 3D file format that is highly optimized for game engines like Godot. It is preferred because:

  • It contains mesh data, textures, materials, and animations in a single file.
  • It has smaller file sizes than other formats like .obj or .fbx.
  • It is fully compatible with Godot without requiring external converters.

Our 000_Snowpuff.glb file contains a fully textured 3D model of Snowpuff, which we will now integrate into the scene.


🔹 Step 6.2: Importing the Model into Godot

Step 1: Copy the Model into the Project Folder

Ensure that your 000_Snowpuff.glb file is stored within the res://Models/ directory. Your project should now look like this:

Project/
├── Models/
│   └── 000_Snowpuff.glb   # Custom 3D Model
├── Scripts/
│   ├── main.gd
│   └── skybox.gd
├── Textures/
│   └── default_sky.hdr
└── project.godot

Step 2: Let Godot Process the Model

Once the .glb file is placed inside the Models/ directory, Godot will automatically detect it in the FileSystem panel .

To verify:

  1. Open Godot Editor .
  2. Navigate to res://Models/ in the FileSystem panel .
  3. Locate 000_Snowpuff.glb and click on it.

At this stage, Godot will generate additional files , including:

  • 000_Snowpuff.material → Stores material settings for the model.
  • 000_Snowpuff.meshes → Stores mesh data.
  • 000_Snowpuff.animations (if applicable).

These are automatically created and used when loading the .glb file into the scene.


🔹 Step 6.3: Replacing the Cube with Snowpuff in main.gd

Now that our model is imported, we need to dynamically load and instantiate it through code in main.gd.

📝 Updated main.gd Code

extends Node3D

var snowpuff: Node3D

# Animate rotation
func _process(delta):
	snowpuff.rotate_y(delta * 0.5)

func _ready():
	# Configure camera
	var camera = Camera3D.new()
	camera.position = Vector3(0, 1, 3)
	camera.look_at(Vector3.ZERO)
	add_child(camera)
	camera.make_current()

	# Add a rotating snowpuff
	var snowpuff_model = load("res://Models/000_Snowpuff.glb")
	if snowpuff_model:
		snowpuff = snowpuff_model.instantiate()
		snowpuff.transform.origin = Vector3(0,0,0)
		add_child(snowpuff)

	# Add world environment
	var skybox = load("res://Scripts/skybox.gd").new()
	add_child(skybox)

🔹 Step 6.4: Breaking Down the Code

1️⃣ Loading the 3D Model

var snowpuff_scene = load("res://Models/000_Snowpuff.glb")
  • load("res://Models/000_Snowpuff.glb") loads the 3D model file.
  • The file must be inside the “res://” directory , otherwise it won’t be found.
  • load() simply loads the file into memory , but does not yet create an instance of it in the game.

2️⃣ Creating an Instance of the Model

if snowpuff_model:
    snowpuff = snowpuff_model.instantiate()
  • .instantiate() creates an actual instance of the model in the scene.
  • This is necessary because .glb files are scenes, not single objects .
  • If this step is skipped, the model will remain loaded in memory but will never appear in the scene .

3️⃣ Positioning the Model

snowpuff.transform.origin = Vector3(0, 0, 0)
  • Moves the model to the center of the scene .
  • If the model appears too high or too low , adjust the Y-axis:
    snowpuff.transform.origin = Vector3(0, -0.5, 0)
    

4️⃣ Rotating the Model

func _process(delta):
    if snowpuff:
        snowpuff.rotate_y(delta * 0.5)
  • The model rotates around the Y-axis over time.
  • delta ensures smooth rotation at any frame rate .

🔹 Step 6.5: Running the Scene

After implementing these changes, running the game should now display:

A skybox background 🌄

A custom 3D model (Snowpuff) instead of a cube ❄️

Smooth rotation animation 🔄

A properly positioned and rendered model 🎨


🔹 Step 6.6: Troubleshooting Common Issues

🔸 The Model Doesn’t Appear

  • Check if the file path is correct :
  var snowpuff_model = load("res://Models/000_Snowpuff.glb")

The file must exist in the correct location.

  • Make sure the model is instantiated :
  if snowpuff_model:
      print("Model loaded successfully")
  else:
      print("Failed to load model")
  • Check if the model is too small or inside the floor :
  • Try scaling it up:
    snowpuff.scale = Vector3(2, 2, 2)
    

🔸 The Model is Too Dark

  • Ensure there is lighting in the scene (Godot does not add lights by default).
  • Add an ambient light source:
    var light = DirectionalLight3D.new()
    light.light_energy = 2.0
    add_child(light)