Instancing

Four hundred boxes, one mesh and one material. SPACE turns instancing on and off, and the panel counts the draw calls it takes either way.


    
main.lua
local natiny = require("natiny")

if not natiny.backend.init(arg and arg[1] or natiny.backend.AUTO) then
    os.exit(1)
end

local window = natiny.window.create("Natiny - Instancing", 960, 640)
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_STRETCH)

local BG = natiny.color.from_hex("#151E2C")
local PLATE = natiny.color.from_hex("#0B121EC8")
local INK = natiny.color.from_hex("#E6EDFA")
local ACCENT = natiny.color.from_hex("#A8C238")

local COLUMNS, ROWS = 20, 20
local SPACING = 1.6
local ORBIT_SPEED = 12.0

local function load_resource(path, make)
    local resource = natiny.resource.load(path)
    local object = make(resource)
    natiny.resource.destroy(resource)
    return object
end

local box_mesh = load_resource("common/models/box_01.glb", natiny.mesh.load)
local shader = load_resource("common/shaders/unlit.nsl", natiny.shader.load)
local font = load_resource("common/fonts/roboto.ttf", function(resource)
    return natiny.font.create(resource, 18)
end)

-- unlit.nsl is the engine's own default material written out on disk, so the
-- boxes look exactly the same whether they are instanced or not.
local material = natiny.material.create(shader)
natiny.material.add_tag(material, "boxes")
natiny.material.set_instanced(material, true)

-- One mesh, one material, four hundred models.
for row = 0, ROWS - 1 do
    for column = 0, COLUMNS - 1 do
        local model = natiny.model.create(box_mesh, material)
        natiny.entity.set_position(model, natiny.entity.SPACE_WORLD,
            (column - (COLUMNS - 1) * 0.5) * SPACING,
            0.0,
            (row - (ROWS - 1) * 0.5) * SPACING)
    end
end

local pivot = natiny.pivot.create()
natiny.entity.set_position(pivot, natiny.entity.SPACE_WORLD, 0.0, 0.0, 0.0)

local camera = natiny.camera.create()
natiny.entity.set_parent(camera, pivot)
natiny.entity.set_position(camera, natiny.entity.SPACE_LOCAL, 0.0, 14.0, 26.0)
natiny.entity.look_at(camera, 0.0, 0.0, 0.0)
natiny.camera.set_fov(camera, 55.0)
natiny.camera.set_clip(camera, 0.1, 200.0)

local MODELS = COLUMNS * ROWS
local PARTS = natiny.mesh.get_part_count(box_mesh)

-- Without instancing every model is drawn on its own, once per mesh part.
-- With instancing the whole grid is one draw per mesh part.
local function draw_calls()
    if natiny.material.get_instanced(material) then
        return PARTS
    end
    return MODELS * PARTS
end

local fps = 60.0

local function draw_panel()
    local instanced = natiny.material.get_instanced(material)
    local lines = {
        { "Instancing: " .. (instanced and "ON" or "OFF"), instanced and ACCENT or INK },
        { "Models: " .. MODELS, INK },
        { "Draw calls: " .. draw_calls(), INK },
        { string.format("FPS: %.0f", fps), INK },
        { "SPACE - toggle instancing", INK },
    }

    local line_height = natiny.font.get_line_height(font)
    local width = 0
    for _, line in ipairs(lines) do
        width = math.max(width, natiny.font.measure_text(font, line[1]))
    end

    natiny.render.set_color(PLATE)
    natiny.render.rounded_rectangle(16, 16, width + 32, line_height * #lines + 32, 10)

    for index, line in ipairs(lines) do
        natiny.render.set_color(line[2])
        natiny.render.text(font, 32, 32 + line_height * (index - 1), line[1])
    end
end

natiny.backend.loop(function(dt)
    if dt > 0 then
        fps = fps + (1 / dt - fps) * math.min(dt * 8, 1)
    end

    if natiny.input.get_key_pressed(natiny.input.KEY_SPACE) then
        natiny.material.set_instanced(material, not natiny.material.get_instanced(material))
    end

    natiny.entity.rotate(pivot, natiny.entity.SPACE_LOCAL, 0.0, ORBIT_SPEED * dt, 0.0)

    natiny.window.bind(window, function()
        natiny.render.clear(BG)
        natiny.camera.bind(camera, function()
            natiny.render.tag("boxes")
        end)
        draw_panel()
    end)
end)

natiny.backend.shutdown()