Here is an editor script I use to set up large models (with 100+ textures) with illumination asset for 3D lighting. It will need to be slightly modified to fit for other models but still should be enough to use as a base:
local json = require "_Scripts.Modules.dkjson"
local M = {}
local DEFAULT_MATERIAL = "/illumination/materials/model.material"
local BASE_TEXTURE_DIR = "/_Assets/BetterCityModel/textures/"
local DATA_TEXTURE = "/illumination/textures/data.png"
local EMPTY_TEXTURE = "/illumination/textures/empty.png"
local function read_text(path)
local f = io.open(path, "r")
if not f then return nil end
local d = f:read("*a")
f:close()
return d
end
local function write_text(path, txt)
local f = io.open(path, "w")
if not f then return false end
f:write(txt)
f:close()
return true
end
local function load_gltf_material_order(gltf_path)
local text = read_text("." .. gltf_path)
if not text then
print("Cannot read GLTF:", gltf_path)
return {}
end
local ok, data = pcall(json.decode, text)
if not ok then
print("Failed to decode GLTF JSON")
return {}
end
if not data.materials then
print("GLTF has no materials[]:", gltf_path)
return {}
end
local order = {}
for _, mat in ipairs(data.materials) do
if mat.name and mat.name ~= "" then
table.insert(order, mat.name)
end
end
return order
end
local function generate_material_block(name)
-- texture paths using your illumination pipeline
local diffuse = BASE_TEXTURE_DIR .. name .. "_baseColor.png"
return string.format([[
materials {
name: "%s"
material: "%s"
textures {
sampler: "DATA_TEXTURE"
texture: "%s"
}
textures {
sampler: "DIFFUSE_TEXTURE"
texture: "%s"
}
textures {
sampler: "LIGHT_TEXTURE"
texture: "%s"
}
textures {
sampler: "NORMAL_TEXTURE"
texture: "%s"
}
textures {
sampler: "SPECULAR_TEXTURE"
texture: "%s"
}
}
]], name, DEFAULT_MATERIAL, DATA_TEXTURE, diffuse, EMPTY_TEXTURE, EMPTY_TEXTURE, EMPTY_TEXTURE)
end
local function update_model(model_path, material_order)
local full = "." .. model_path
local text = read_text(full)
if not text then
print("Cannot read model:", model_path)
return
end
-- Remove ALL existing material blocks safely
text = text:gsub("materials%s*{.-\n}", "")
-- Generate new blocks in EXACT GLTF ORDER
local blocks = {}
for _, name in ipairs(material_order) do
table.insert(blocks, generate_material_block(name))
end
text = text .. "\n" .. table.concat(blocks, "\n")
if write_text(full, text) then
print("Model updated:", model_path)
else
print("Failed to write model:", model_path)
end
end
function M.get_commands()
return {
{
label = "Apply Material And Textures",
locations = { "Assets" },
query = { selection = { type = "resource", cardinality = "one" } },
active = function(opts)
local path = editor.get(opts.selection, "path")
return path and path:match("%.model$")
end,
run = function(opts)
local model_path = editor.get(opts.selection, "path")
local text = read_text("." .. model_path)
if not text then return end
-- extract mesh path
local mesh = text:match('mesh:%s+"([^"]+)"')
if not mesh then
editor.ui.show_dialog(editor.ui.dialog({
title = "Error",
content = editor.ui.label({ text = "No mesh: \"\" found in model." }),
buttons = { editor.ui.dialog_button({ text = "OK", default = true }) }
}))
return
end
-- extract material order from GLTF
local order = load_gltf_material_order(mesh)
if #order == 0 then
editor.ui.show_dialog(editor.ui.dialog({
title = "Error",
content = editor.ui.label({ text = "No materials[] found in GLTF: " .. mesh }),
buttons = { editor.ui.dialog_button({ text = "OK", default = true }) }
}))
return
end
update_model(model_path, order)
editor.ui.show_dialog(editor.ui.dialog({
title = "Done",
content = editor.ui.label({
text = "Material textures applied.\nOrder preserved from: " .. mesh
}),
buttons = { editor.ui.dialog_button({ text = "OK", default = true }) }
}))
end
}
}
end
return M