Creating meshes procedurally — code sample - Stingray Lua API Reference

Creating meshes procedurally — code sample

Description

This example shows how to use the stingray.ProceduralMesh interface to create meshes on the fly:

Procedural meshes

To use this code in a project, first save it in a .lua file in your project. Then, in one of your project scripts, require the file and cache its return in a local variable:

local proc_mesh_controller = require 'script/lua/proc_mesh_controller'

You have to call three functions on this variable:

  • proc_mesh_controller:start(): call this when a level is first loaded. If you're using the Appkit, you can do it in the Project.on_level_load_pre_flow() function.

  • proc_mesh_controller:update(dt): call this each frame, and pass the delta time since the last frame. If you're using the Appkit, you can do it in the Project.update(dt) function.

  • proc_mesh_controller:shutdown(): call this when your level is unloaded. If you're using the Appkit, you can do it in the Project.on_level_shutdown_post_flow() function.

Code

require('core/appkit/lua/class')

ProcMeshController = Appkit.class(ProcMeshController)
local M = ProcMeshController

Vector3ArrayClass = Appkit.class(Vector3ArrayClass)
local Vector3Array = Vector3ArrayClass

function Vector3Array:init()
    self.x = stingray.Float32Array()
    self.y = stingray.Float32Array()
    self.z = stingray.Float32Array()
end

function Vector3Array.splat(f)
    local res = Vector3Array()
    res.x = f
    res.y = f
    res.z = f
    return res
end

function Vector3Array:push(v)
    self.x:push(v.x)
    self.y:push(v.y)
    self.z:push(v.z)
end

function Vector3Array.add(a,b)
    local res = Vector3Array()
    res.x = stingray.Float32Array.add(a.x, b.x)
    res.y = stingray.Float32Array.add(a.y, b.y)
    res.z = stingray.Float32Array.add(a.z, b.z)
    return res
end

function Vector3Array.__add(a,b) return Vector3Array.add(a,b) end

function Vector3Array.sub(a,b)
    local res = Vector3Array()
    res.x = stingray.Float32Array.sub(a.x, b.x)
    res.y = stingray.Float32Array.sub(a.y, b.y)
    res.z = stingray.Float32Array.sub(a.z, b.z)
    return res
end

function Vector3Array.__sub(a,b) return Vector3Array.sub(a,b) end

function Vector3Array.mul(a,b)
    local res = Vector3Array()
    res.x = stingray.Float32Array.mul(a.x, b.x)
    res.y = stingray.Float32Array.mul(a.y, b.y)
    res.z = stingray.Float32Array.mul(a.z, b.z)
    return res
end

function Vector3Array.__mul(a,b) return Vector3Array.mul(a,b) end

function Vector3Array.div(a,b)
    local res = Vector3Array()
    res.x = stingray.Float32Array.div(a.x, b.x)
    res.y = stingray.Float32Array.div(a.y, b.y)
    res.z = stingray.Float32Array.div(a.z, b.z)
    return res
end

function Vector3Array.__div(a,b) return Vector3Array.div(a,b) end

function Vector3Array.__unm(a)
    local res = Vector3Array()
    res.x = -a.x
    res.y = -a.y
    res.z = -a.z
    return res
end

function Vector3Array.__div(a,b) return Vector3Array.div(a,b) end

function Vector3Array.length(a)
    return stingray.Float32Array.sqrt(a.x*a.x + a.y*a.y + a.z*a.z)
end

function Vector3Array.cross(a,b)
    local res = Vector3Array()
    res.x = (a.y*b.z)-(a.z*b.y)
    res.y = (a.z*b.x)-(a.x*b.z)
    res.z = (a.x*b.y)-(a.y*b.x)
    return res
end

function Vector3Array.interleave(arrays)
    local t = {}
    for _,a in ipairs(arrays) do
        t[#t+1] = a.x
        t[#t+1] = a.y
        t[#t+1] = a.z
    end
    return stingray.Float32Array.interleave(t)
end

local function append_v3(buffer, ...)
    local nb = #buffer
    local n = select('#', ...)
    for i=1,n do
        local v = select(i, ...)
        buffer[nb + (i-1)*3 + 1] = v.x
        buffer[nb + (i-1)*3 + 2] = v.y
        buffer[nb + (i-1)*3 + 3] = v.z
    end
end

local function append_f(buffer, ...)
    local nb = #buffer
    local n = select('#', ...)
    for i=1,n do
        buffer[nb + i] = select(i, ...)
    end
end

local function extruded_triangle(iterations, v1, v2, v3, vbuffer, ibuffer, ignorebase)
    local n = stingray.Vector3.normalize(stingray.Vector3.cross(v2-v1, v3-v1))
    if iterations == 0 then
        local i = #vbuffer / 6
        append_v3(vbuffer, v1, n, v2, n, v3, n)
        append_f(ibuffer, i, i+1, i+2)
    else
        local v4 = (v1 + v2) / 2
        local v5 = (v2 + v3) / 2
        local v6 = (v3 + v1) / 2
        local a = stingray.Vector3.length(v2-v1) / 2
        local h = a * math.sqrt(6) / 3
        local v7 = (v1 + v2 + v3) / 3 + n * h / 2
        if not ignorebase then
            extruded_triangle(iterations-1, v1, v4, v6, vbuffer, ibuffer)
            extruded_triangle(iterations-1, v4, v2, v5, vbuffer, ibuffer)
            extruded_triangle(iterations-1, v6, v5, v3, vbuffer, ibuffer)
        end

        extruded_triangle(iterations-1, v7, v6, v4, vbuffer, ibuffer)
        extruded_triangle(iterations-1, v7, v4, v5, vbuffer, ibuffer)
        extruded_triangle(iterations-1, v7, v5, v6, vbuffer, ibuffer)
    end
end

local function menger_sponge(iterations, vmin, vmax, vbuffer, ibuffer)
    local bc = stingray.Script.temp_byte_count()
    if iterations == 0 then
        local i = #vbuffer / 6
        local v1 = vmin
        local v2 = stingray.Vector3(vmax.x, vmin.y, vmin.z)
        local v3 = stingray.Vector3(vmin.x, vmax.y, vmin.z)
        local v4 = stingray.Vector3(vmax.x, vmax.y, vmin.z)
        local v5 = stingray.Vector3(vmin.x, vmin.y, vmax.z)
        local v6 = stingray.Vector3(vmax.x, vmin.y, vmax.z)
        local v7 = stingray.Vector3(vmin.x, vmax.y, vmax.z)
        local v8 = vmax
        local x = stingray.Vector3(1,0,0)
        local y = stingray.Vector3(0,1,0)
        local z = stingray.Vector3(0,0,1)

        append_v3(vbuffer,
            v1, -y, v2, -y, v6, -y, v5, -y,
            v4, y, v3, y, v7, y, v8, y,
            v2, x, v4, x, v8, x, v6, x,
            v3, -x, v1, -x, v5, -x, v7, -x,
            v3, -z, v4, -z, v2, -z, v1, -z,
            v5, z, v6, z, v8, z, v7, z
        )
        append_f(ibuffer, i, i+1, i+2, i, i+2, i+3) i = i + 4
        append_f(ibuffer, i, i+1, i+2, i, i+2, i+3) i = i + 4
        append_f(ibuffer, i, i+1, i+2, i, i+2, i+3) i = i + 4
        append_f(ibuffer, i, i+1, i+2, i, i+2, i+3) i = i + 4
        append_f(ibuffer, i, i+1, i+2, i, i+2, i+3) i = i + 4
        append_f(ibuffer, i, i+1, i+2, i, i+2, i+3) i = i + 4
    else
        for x = 1,3 do
            for y = 1,3 do
                for z = 1,3 do
                    if (x ~= 2 and y ~= 2) or (y ~= 2 and z ~= 2) or (x ~= 2 and z ~= 2) then
                        local d = (vmax - vmin)/3
                        local xyz = stingray.Vector3(x, y, z)
                        menger_sponge(iterations-1,
                            vmin + stingray.Vector3.multiply_elements(xyz - stingray.Vector3(1,1,1), d),
                            vmin + stingray.Vector3.multiply_elements(xyz, d),
                            vbuffer, ibuffer)
                    end
                end
            end
        end
    end
    stingray.Script.set_temp_byte_count(bc)
end

local function make_sphere_points(N)
    local sphere = {n = Vector3Array()}

    local a = 4 * math.pi / N
    local d = math.sqrt(a)
    local M_theta = math.floor(math.pi / d + 0.5)
    local d_theta = math.pi / M_theta
    local d_phi = a / d_theta
    for m = 0,M_theta-1 do
        local theta = math.pi * (m + 0.5) / M_theta
        local M_phi = math.floor(2*math.pi*math.sin(theta)/d_phi + 0.5)
        for n = 0,M_phi-1 do
            local phi = 2 * math.pi * n / M_phi
            local x = math.sin(theta) * math.cos(phi)
            local y = math.sin(theta) * math.sin(phi)
            local z = math.cos(theta)
            local n = stingray.Vector3(x, y, z)
            sphere.n:push(n)
        end
    end

    sphere.u = Vector3Array.sub( stingray.Vector3(0,0,1), sphere.n * Vector3Array.splat(sphere.n.z) )
    sphere.u = sphere.u / Vector3Array.splat(Vector3Array.length(sphere.u))
    sphere.r = Vector3Array.cross(sphere.n, sphere.u)

    return sphere
end

local function make_patch_sphere(sphere_points, r, d, vbuffer, ibuffer)
    local pos = sphere_points.n * stingray.Vector3(r,r,r)
    local x = sphere_points.u
    local y = sphere_points.r
    local z = sphere_points.n
    local rx = stingray.Vector3(d,d,d)
    local ry = stingray.Vector3(d,d,d)
    local rz = stingray.Vector3(d,d,d)*0.1

    local v1 = pos - x*rx - y*ry - z*rz
    local v2 = pos + x*rx - y*ry - z*rz
    local v3 = pos - x*rx + y*ry - z*rz
    local v4 = pos + x*rx + y*ry - z*rz
    local v5 = pos - x*rx - y*ry + z*rz
    local v6 = pos + x*rx - y*ry + z*rz
    local v7 = pos - x*rx + y*ry + z*rz
    local v8 = pos + x*rx + y*ry + z*rz

    local interleaved = Vector3Array.interleave(
        {v1, -y, v2, -y, v6, -y, v5, -y,
        v4, y, v3, y, v7, y, v8, y,
        v2, x, v4, x, v8, x, v6, x,
        v3, -x, v1, -x, v5, -x, v7, -x,
        v3, -z, v4, -z, v2, -z, v1, -z,
        v5, z, v6, z, v8, z, v7, z}
    )
    stingray.Float32Array.swap(vbuffer, interleaved)

    if ibuffer then
        local count = #sphere_points.n.x
        ibuffer:resize(0)
        for n=0,count-1 do
            local i = 24*n
            ibuffer:push(i, i+1, i+2, i, i+2, i+3) i = i + 4
            ibuffer:push(i, i+1, i+2, i, i+2, i+3) i = i + 4
            ibuffer:push(i, i+1, i+2, i, i+2, i+3) i = i + 4
            ibuffer:push(i, i+1, i+2, i, i+2, i+3) i = i + 4
            ibuffer:push(i, i+1, i+2, i, i+2, i+3) i = i + 4
            ibuffer:push(i, i+1, i+2, i, i+2, i+3) i = i + 4
        end
    end
end

function M:shutdown()
    for _,mesh in ipairs(self.meshes) do
        self:destroy_procmesh(mesh)
    end
end

function M:start()

    self.spinning_units = {}
    self.meshes = {}

    local make_menger_sponge = function(pos, iter)
        local vbuffer = {}
        local ibuffer = {}
        local s = 1
        menger_sponge(iter, stingray.Vector3(-s,-s,0), stingray.Vector3(s,s,2*s), vbuffer, ibuffer)

        local unit = stingray.World.spawn_unit(stingray.Application.main_world(), "core/units/camera", pos)
        local mesh = self:make_procmesh(unit, "rp_root", vbuffer, ibuffer)

        table.insert(self.spinning_units, unit)
        table.insert(self.meshes, mesh)
    end

    make_menger_sponge(stingray.Vector3(-4,-6,1), 0)
    make_menger_sponge(stingray.Vector3(-4,-2,1), 1)
    make_menger_sponge(stingray.Vector3(-4, 2,1), 2)
    make_menger_sponge(stingray.Vector3(-4, 6,1), 3)

    self.sphere_points = make_sphere_points(300, self.sphere_points)
    local vbuffer = stingray.Float32Array()
    local ibuffer = stingray.Float32Array()
    make_patch_sphere(self.sphere_points, 3, 0.2, vbuffer, ibuffer)
    local unit = stingray.World.spawn_unit(stingray.Application.main_world(), "core/units/camera", stingray.Vector3(-10, 0, 5))
    self.sphere = self:make_procmesh(unit, "rp_root", vbuffer, ibuffer, {vertex_buffer_validity = stingray.RenderBuffer.RB_VALIDITY_UPDATABLE})
    table.insert(self.spinning_units, unit)
    table.insert(self.meshes, self.sphere)
end

function M:make_procmesh(unit, node, vertices, indices, options)
    local t = {}
    options = options or {}
    local xyz32 = stingray.RenderBuffer.format(stingray.RenderBuffer.RB_FLOAT_COMPONENT, true, false, 32, 32, 32, 0)
    local channels = {
        {format = xyz32, semantic = stingray.RenderBuffer.RB_POSITION_SEMANTIC, vb_index = 0, set = 0, instance = false},
        {format = xyz32, semantic = stingray.RenderBuffer.RB_NORMAL_SEMANTIC, vb_index = 0, set = 0, instance = false}
    }
    t.vdecl = stingray.RenderBuffer.create_description(stingray.RenderBuffer.RB_VERTEX_DESCRIPTION, channels)
    local a = 20
    local R = a * math.sqrt(3) / 3
    local r = R / 2

    t.vbuffer = stingray.RenderBuffer.create_buffer(options.vertex_buffer_validity or stingray.RenderBuffer.RB_VALIDITY_STATIC,
        stingray.RenderBuffer.RB_VERTEX_BUFFER_VIEW, 6*4, vertices)
    t.ibuffer = stingray.RenderBuffer.create_buffer(stingray.RenderBuffer.RB_VALIDITY_STATIC,
        stingray.RenderBuffer.RB_INDEX_BUFFER_VIEW, 4, indices)

    t.mesh = stingray.ProceduralMesh.create(unit, node, "procedural-mesh", stingray.ProceduralMesh.MO_VIEWPORT_VISIBLE_FLAG + stingray.ProceduralMesh.MO_SHADOW_CASTER_FLAG)

    stingray.ProceduralMesh.set_bounding_box(t.mesh, stingray.Vector3(-100, -100, -100), stingray.Vector3(100, 100, 100))
    local batches = {{
        primitive_type = stingray.ProceduralMesh.MO_TRIANGLE_LIST,
        material_index = 0,
        vertex_offset = 0,
        primitives = #indices / 3,
        index_offset = 0,
        vertices = 0,
        instances = 1
    }}
    stingray.ProceduralMesh.set_batch_info(t.mesh, batches)
    stingray.ProceduralMesh.add_resource(t.mesh, t.vdecl)
    stingray.ProceduralMesh.add_resource(t.mesh, t.vbuffer)
    stingray.ProceduralMesh.add_resource(t.mesh, t.ibuffer)

    stingray.ProceduralMesh.set_materials(t.mesh, {"core/stingray_renderer/shader_import/no_uvs"})
    return t
end

function M:destroy_procmesh(t)
    stingray.ProceduralMesh.destroy(t.mesh)

    stingray.RenderBuffer.destroy_buffer(t.ibuffer)
    stingray.RenderBuffer.destroy_buffer(t.vbuffer)
    stingray.RenderBuffer.destroy_description(t.vdecl)
end

function M:update(dt)

    self.t = (self.t or 0) + dt
    local rot = stingray.Quaternion.axis_angle(stingray.Vector3(0,0,1), self.t/2)
    for _,unit in ipairs(self.spinning_units) do
        stingray.Unit.set_local_rotation(unit, 1, rot)
    end

    if self.sphere then
        self.sphere_vbuffer = self.sphere_vbuffer or stingray.Float32Array()
        self.sphere_vbuffer:resize(0)
        make_patch_sphere(self.sphere_points, 1.8 + (math.sin(self.t*4)+1)*1.5, 0.2, self.sphere_vbuffer, nil)
        stingray.RenderBuffer.update_buffer(self.sphere.vbuffer, stingray.RenderBuffer.RB_FLOAT_COMPONENT, self.sphere_vbuffer)
    end

end

return M