Imported from FilthyFrost/load-3d-model-lua (
SKILL.md). Install upstream withnpx skills add FilthyFrost/load-3d-model-lua. Copyright stays with the author.
Load 3D Models in Lua Game Environments
Overview
This skill enables AI agents to load .glb, .gltf, and .obj 3D model files into Lua-based game environments. It covers three scenarios: TapTap Maker (UrhoX engine), LOVR framework, and pure Lua parsing.
Core principle: Know your target engine's native format, convert if needed, then use the engine's API to load and place the model in the scene.
When to Use
- AI agent needs to load a
.glb/.gltf/.objfile into a Lua game - Building a 3D scene in TapTap Maker (TapTap 制造)
- Building a 3D game/app with LOVR
- Need to parse GLB binary data in pure Lua 5.4
- Need to convert between 3D formats for Lua engine compatibility
When NOT to Use
- Target engine is NOT Lua-based (use engine-native docs instead)
- Only need 2D sprites/images (no 3D model loading needed)
Quick Reference
| Engine / Framework | .glb | .gltf | .obj | Native Format | Difficulty |
|---|---|---|---|---|---|
| TapTap Maker (UrhoX) | needs conversion | needs conversion | needs conversion | .mdl |
medium |
| LOVR | native | native | native | glTF/GLB | easy |
| LOVE 2D + 3DreamEngine | native | native | native | glTF | easy |
| LOVE 2D + g3d | no | no | native | OBJ | easy |
| Pure Lua 5.4 | manual parse | manual parse | manual parse | N/A | hard |
Scenario 1: TapTap Maker (UrhoX Engine)
TapTap Maker uses the UrhoX engine (based on Urho3D). The engine's native 3D model format is .mdl, NOT .glb.
Step 1: Format Conversion (GLB to MDL)
GLB files must be converted to .mdl before use. Options:
Option A: Blender + Urho3D Exporter (Recommended)
- Open
.glbin Blender - Install Urho3D Blender exporter addon
- Export as
.mdl+ materials
Option B: AssetImporter CLI (Urho3D built-in tool)
# Convert FBX/OBJ/DAE to MDL (GLB support is limited)
AssetImporter model input.fbx output.mdl
Note: AssetImporter's GLB support is unreliable. Convert GLB to FBX/OBJ first if needed.
Option C: Let TapTap Maker AI handle it When describing your game to TapTap Maker's AI, specify:
"Use this 3D model file [filename] as [object name] in the scene"
The AI agent may handle conversion internally.
Step 2: Load Model in UrhoX Lua
-- Get the resource cache
local cache = GetCache()
-- Create a scene node
local node = scene:CreateChild("MyObject")
node.position = Vector3(0, 1, 0) -- x, y, z position
node.rotation = Quaternion(0, 90, 0) -- pitch, yaw, roll
node.scale = Vector3(1, 1, 1) -- scale
-- Add a StaticModel component and load the .mdl file
local model = node:CreateComponent("StaticModel")
model.model = cache:GetResource("Model", "Models/MyModel.mdl")
model.material = cache:GetResource("Material", "Materials/MyMaterial.xml")
Step 3: Animated Models
-- For models with skeletal animation
local animModel = node:CreateComponent("AnimatedModel")
animModel.model = cache:GetResource("Model", "Models/Character.mdl")
animModel.material = cache:GetResource("Material", "Materials/Character.xml")
-- Play animation
local animCtrl = node:CreateComponent("AnimationController")
animCtrl:PlayExclusive("Models/Character_Walk.ani", 0, true, 0.2)
Step 4: Common Scene Setup
-- Full scene example for TapTap Maker / UrhoX
function CreateScene()
scene = Scene()
scene:CreateComponent("Octree")
scene:CreateComponent("PhysicsWorld")
-- Camera
local cameraNode = scene:CreateChild("Camera")
cameraNode.position = Vector3(0, 5, -10)
cameraNode:LookAt(Vector3(0, 0, 0))
local camera = cameraNode:CreateComponent("Camera")
-- Light
local lightNode = scene:CreateChild("Light")
lightNode.direction = Vector3(0.6, -1.0, 0.8)
local light = lightNode:CreateComponent("Light")
light.lightType = LIGHT_DIRECTIONAL
light.color = Color(1, 1, 1)
-- Load 3D model
local objectNode = scene:CreateChild("House")
objectNode.position = Vector3(0, 0, 0)
local object = objectNode:CreateComponent("StaticModel")
object.model = cache:GetResource("Model", "Models/House.mdl")
object.material = cache:GetResource("Material", "Materials/House.xml")
-- Add physics (optional)
local body = objectNode:CreateComponent("RigidBody")
local shape = objectNode:CreateComponent("CollisionShape")
shape:SetTriangleMesh(object.model)
end
UrhoX File Structure
project/
Data/
Models/
MyModel.mdl <- 3D model (converted from GLB)
MyModel_Walk.ani <- animation (if any)
Materials/
MyMaterial.xml <- material definition
Textures/
MyTexture.png <- textures
Scripts/
main.lua <- your game code
Scenario 2: LOVR Framework
LOVR natively supports .glb, making it the easiest path.
Load and Display
function lovr.load()
-- Load GLB directly - no conversion needed
model = lovr.graphics.newModel('house.glb')
end
function lovr.draw(pass)
-- Draw at position (x=0, y=1, z=-3), scale=1
pass:draw(model, 0, 1, -3, 1)
end
Multiple Models in a Scene
local models = {}
function lovr.load()
models.house = lovr.graphics.newModel('house.glb')
models.tree = lovr.graphics.newModel('tree.glb')
models.car = lovr.graphics.newModel('car.obj') -- OBJ also works
end
function lovr.draw(pass)
pass:draw(models.house, 0, 0, -5)
pass:draw(models.tree, 3, 0, -5)
pass:draw(models.car, -3, 0, -5, 1, 0, math.pi/4, 0, 1) -- with rotation
end
With Camera Controls
local model
local position = lovr.math.newVec3(0, 1.7, 0)
local angle = 0
function lovr.load()
model = lovr.graphics.newModel('scene.glb')
end
function lovr.update(dt)
-- Simple WASD movement
if lovr.system.isKeyDown('w') then
position.z = position.z - 2 * dt
end
if lovr.system.isKeyDown('s') then
position.z = position.z + 2 * dt
end
if lovr.system.isKeyDown('a') then
position.x = position.x - 2 * dt
end
if lovr.system.isKeyDown('d') then
position.x = position.x + 2 * dt
end
end
function lovr.draw(pass)
pass:draw(model, 0, 0, 0)
end
LOVR Project Structure
my-game/
main.lua <- your code
house.glb <- 3D models (GLB, GLTF, OBJ all work)
tree.glb
texture.png <- textures (if separate from GLB)
Run LOVR
# macOS
/Applications/lovr.app/Contents/MacOS/lovr ./my-game
# Windows: drag folder onto lovr.exe
# Or from command line:
lovr.exe ./my-game
Scenario 3: Pure Lua 5.4 GLB Parser
When you need to parse .glb data without an engine (e.g., for data extraction or custom pipeline).
GLB File Structure
Offset Size Content
------ ---- -------
0 4 Magic: "glTF" (0x46546C67)
4 4 Version: 2
8 4 Total file length
12 4 Chunk 0 length (JSON)
16 4 Chunk 0 type: "JSON" (0x4E4F534A)
20 N Chunk 0 data: JSON string
20+N 4 Chunk 1 length (BIN)
24+N 4 Chunk 1 type: "BIN\0" (0x004E4942)
28+N M Chunk 1 data: binary buffer
Parser Code
-- glb_parser.lua
-- Parse a .glb file into JSON metadata + binary buffer
-- Requires: Lua 5.4 (for string.unpack)
local glb = {}
function glb.parse(filepath)
-- Read entire file
local f = assert(io.open(filepath, "rb"))
local data = f:read("*a")
f:close()
-- Parse header (12 bytes)
local magic, version, total_length = string.unpack("<c4I4I4", data)
assert(magic == "glTF", "Not a valid GLB file: " .. filepath)
assert(version == 2, "Only glTF 2.0 supported, got version " .. version)
local result = {
version = version,
total_length = total_length,
json = nil,
bin = nil,
}
-- Parse chunks
local offset = 13 -- 1-based index after header
while offset < #data do
local chunk_length, chunk_type = string.unpack("<I4c4", data, offset)
local chunk_data_start = offset + 8
local chunk_data = data:sub(chunk_data_start, chunk_data_start + chunk_length - 1)
if chunk_type == "JSON" then
result.json = chunk_data
elseif chunk_type == "BIN\0" then
result.bin = chunk_data
end
offset = chunk_data_start + chunk_length
end
return result
end
--- Extract vertex positions from parsed GLB
--- Returns a list of {x, y, z} tables
function glb.extract_vertices(parsed)
if not parsed.json then return {} end
-- Decode JSON (requires a JSON library, e.g., dkjson or cjson)
local json = require("dkjson") -- or cjson, or your preferred JSON lib
local gltf = json.decode(parsed.json)
local vertices = {}
for _, mesh in ipairs(gltf.meshes or {}) do
for _, primitive in ipairs(mesh.primitives or {}) do
local pos_idx = primitive.attributes and primitive.attributes.POSITION
if pos_idx then
local accessor = gltf.accessors[pos_idx + 1] -- 0-indexed to 1-indexed
local bv = gltf.bufferViews[accessor.bufferView + 1]
local byte_offset = (bv.byteOffset or 0) + (accessor.byteOffset or 0) + 1
for i = 0, accessor.count - 1 do
local x, y, z = string.unpack("<fff", parsed.bin, byte_offset + i * 12)
vertices[#vertices + 1] = {x = x, y = y, z = z}
end
end
end
end
return vertices
end
return glb
Usage
local glb = require("glb_parser")
-- Parse file
local model = glb.parse("house.glb")
print("JSON length:", #model.json)
print("BIN length:", #model.bin)
-- Extract vertices
local verts = glb.extract_vertices(model)
print("Vertex count:", #verts)
for i, v in ipairs(verts) do
print(string.format(" v[%d] = (%.2f, %.2f, %.2f)", i, v.x, v.y, v.z))
end
Format Conversion Cheatsheet
| From | To | Tool | Command |
|---|---|---|---|
.glb |
.gltf |
Blender | Open GLB, Export as glTF Separate |
.glb |
.obj |
Blender | Open GLB, Export as Wavefront OBJ |
.glb |
.mdl (UrhoX) |
Blender + Urho3D addon | Open GLB, Export with Urho3D exporter |
.obj |
.glb |
Blender | Open OBJ, Export as glTF Binary |
.obj |
.mdl (UrhoX) |
Blender + Urho3D addon | Open OBJ, Export with Urho3D exporter |
.fbx |
.mdl (UrhoX) |
AssetImporter | AssetImporter model input.fbx output.mdl |
Blender CLI batch conversion example:
blender --background --python convert.py -- input.glb output.obj
Common Mistakes
| Mistake | Fix |
|---|---|
Loading .glb directly in UrhoX |
UrhoX requires .mdl format. Convert first. |
| Forgetting materials/textures | GLB embeds textures; after conversion, ensure materials are exported too |
| Wrong coordinate system | Urho3D uses Y-up left-handed; Blender uses Z-up. Check orientation after import. |
| Model too big/small in scene | Adjust node.scale or re-export with correct scale in Blender |
| Missing JSON library for pure Lua parser | Install dkjson via LuaRocks: luarocks install dkjson |
Using lua-gltf library for .glb files |
lua-gltf only supports JSON .gltf, NOT binary .glb |
| Forgetting Octree component in UrhoX | Scene must have scene:CreateComponent("Octree") or nothing renders |
AI Agent Workflow
When an AI agent needs to add a 3D model to a Lua game, follow this decision tree:
1. What engine?
├── TapTap Maker / UrhoX
│ ├── Is model already .mdl? → Load directly with cache:GetResource
│ └── Is model .glb/.obj? → Convert to .mdl first, then load
│
├── LOVR
│ └── Load directly: lovr.graphics.newModel('file.glb')
│
└── Pure Lua / Custom engine
└── Use glb_parser.lua to extract geometry data
└── Feed vertices/indices to your renderer
2. Place in scene:
├── Set position (Vector3 or x,y,z)
├── Set rotation (Quaternion or angles)
├── Set scale
└── Add physics body if needed
Dependencies Summary
| Component | Install | Purpose |
|---|---|---|
| LOVR | lovr.org download | GLB/GLTF/OBJ rendering engine |
| dkjson | luarocks install dkjson |
JSON parsing for pure Lua GLB parser |
| Blender | blender.org download | Format conversion (GLB to MDL/OBJ) |
| Urho3D Blender addon | GitHub urho3d/urho3d | Export .mdl from Blender |