← Tech 01 / Engine project

Laguna

A game engine and world-authoring environment built around GPU-driven rendering, procedural placement and shared network state.

Laguna editor showing a painted deciduous forest ecotope and placement controls
GPU instancing and ecotope painting in Laguna.

Skinned instanced renderer.

Laguna bakes the bone matrices for every animation frame into a floating-point texture. The vertex shader samples those matrices and performs the skinning, while each unit only supplies its transform, pose, time offset, speed and tint.

Hundreds of units can therefore share the same mesh, material and animation texture while moving through an animation independently. The animation work combines the sparse, procedural approach described by David Rosen with the texture-driven crowd-rendering method in GPU Gems 3.

01

One texture, different animation state.

The baker writes four texels for every bone matrix. At draw time, instance data selects the pose and changes time offset and speed; the shader reconstructs the matrices and skins each vertex.

laguna/src/animation_texture.lua Lua
local matrix_texels = frame_count * num_bones * 4
local texel_count = header_texels + matrix_texels

for frame_idx = 0, action.frameCount - 1 do
    for bone_idx = 0, num_bones - 1 do
        local bm = create_bone_matrix(
            anim_model.model.bindPose[bone_idx],
            action.framePoses[frame_idx][bone_idx]
        )
        local texel_idx = frame_data_offset +
            (global_frame_idx + frame_idx) * num_bones * 4 +
            bone_idx * 4

        write_texel(image_data, texel_idx + 0,
            bm.m0, bm.m1, bm.m2, bm.m3)
        write_texel(image_data, texel_idx + 1,
            bm.m4, bm.m5, bm.m6, bm.m7)
        write_texel(image_data, texel_idx + 2,
            bm.m8, bm.m9, bm.m10, bm.m11)
        write_texel(image_data, texel_idx + 3,
            bm.m12, bm.m13, bm.m14, bm.m15)
    end
end
laguna/resources/shaders/dlgl_skinned_crowd_v.glsl GLSL
int pose_id = max(0, int(round(a_anim.x)));
float speed = abs(a_anim.z) > 0.0001 ? a_anim.z : 1.0;
float animation_time = u_anim0.x * speed + a_anim.y;

mat4 skin = mat4(0.0);
for (int i = 0; i < 4; i++) {
    float weight = a_boneWeights[i];
    if (weight > 0.0) {
        skin += weight * boneMatrix(
            int(round(a_boneIds[i])),
            pose_id,
            animation_time
        );
    }
}

GPU placement & map authoring.

The authored output is a set of maps and rules rather than a manually maintained list of every tree, rock or prop. A brush writes density into a texture; the GPU generates stable candidate positions and uses deterministic dithering to decide which candidates are kept.

Density, asset ranges, terrain pages, slope, height, masks and detail pressure can all affect the result. Because the candidates are derived from world-space cells and a seed, the same maps produce the same placement.

02

Paint density, derive instances.

The authoring shader adds or removes a soft brush from the density texture. The placement shader samples that texture and compares it with a repeatable hash for each world cell, producing a stable dithered distribution.

laguna/resources/shaders/gpu_density_stamp_f.glsl GLSL
vec2 uv = gl_FragCoord.xy * (1.0 / mapSize);
float currentDensity = texture(sourceMap, uv).r;
float falloff = stampFalloff(length(uv - stampCenter));
float nextDensity = clamp(
    currentDensity + stampSign * falloff,
    0.0,
    1.0
);
finalColor = vec4(
    nextDensity,
    nextDensity,
    nextDensity,
    1.0
);
laguna/resources/shaders/dlgl_placement.glsl GLSL
vec2 cell_id = center_cell + vec2(ix, iz) -
    vec2(floor(side * 0.5));
vec2 hash_cell = cell_id + seed * vec2(0.013, 0.021);
vec2 jitter = (laguna_hash22(hash_cell + 3.1) - 0.5) *
    2.0 * min(jitter_radius, safe_cell_size * 0.86);

float density = texelFetch(
    density_map,
    density_texel,
    0
).r * in_density;
float density_keep = has_density * step(
    laguna_hash21(hash_cell + 11.4),
    density
);
float keep = density_keep * in_slot * in_radius *
    active_candidate;

Networked world authoring.

Laguna is networked from the ground up. Player and editor actions enter the world through the same event path. In a networked session they are serialised, given a canonical order and broadcast; a joining peer receives a snapshot containing world state and persistent editing events.

That architecture is the foundation for people joining the same world, working in different scenes or areas and hopping in and out without trying to diff scene files. Editor authority is currently host-led while the multi-editor permission model is opened up.

A running Blood match in Laguna with opposing groups of units and the multiplayer RTS interface
A running Laguna world; simulation and authored world changes share the same event-based foundation.
03

World changes are network events.

Systems submit actions to the world without needing a separate offline and online path. Snapshots preserve terrain, road and instance-paint events so a new peer can reconstruct the authored state on arrival.

laguna/src/ecs.lua Lua
function World:submit_event(event)
    if self.network_submit_event then
        return self.network_submit_event(self, event)
    end
    return self:insert_event(event)
end
laguna/src/network_snapshot.lua Lua
local PERSISTENT_EVENT_TYPES = {
    instance_paint = true,
    road_spline_point = true,
    terrain_paint = true,
    terrain_generate = true,
}

for _, event in ipairs(world.events) do
    if PERSISTENT_EVENT_TYPES[event.type] then
        snapshot.persistent_events[
            #snapshot.persistent_events + 1
        ] = event:serialise()
    end
end

More technical work.

Return to the Tech page for NaturalMotion tools and the wider technical-design index.

Back to Tech