Files
retro3d/resources.cm
2025-12-17 00:49:13 -06:00

553 lines
15 KiB
Plaintext

// Resources module for lance3d - sprite and model loading
var io = use('fd')
var gltf = use('mload/gltf')
var model_c = use('model')
var png = use('cell-image/png')
var resize_mod = use('cell-image/resize')
var anim_mod = use('animation')
var skin_mod = use('skin')
// Backend reference (set by core)
var _backend = null
// Texture original data storage for re-resizing on style change
var TEX_ORIGINAL = key("texture_original")
// Default material prototype
var _default_material = {
color_map: null,
paint: [1, 1, 1, 1],
coverage: "opaque",
face: "single",
lamp: "lit"
}
function set_backend(backend) {
_backend = backend
}
// Get texture size for platform and tier
function get_tex_size(style, tier) {
if (!style) return 64
var sizes = style.tex_sizes
if (tier == "hero" && sizes.hero) return sizes.hero
if (tier == "low" && sizes.low) return sizes.low
return sizes.normal || 64
}
// Resize an image to platform-appropriate size
function resize_image_for_platform(img, style, tier) {
var target_size = get_tex_size(style, tier)
var src_w = img.width
var src_h = img.height
// If already at or below target size, return as-is
if (src_w <= target_size && src_h <= target_size) {
return img
}
// Resize to fit within target_size x target_size (square)
var scale = target_size / number.max(src_w, src_h)
var dst_w = number.floor(src_w * scale)
var dst_h = number.floor(src_h * scale)
if (dst_w < 1) dst_w = 1
if (dst_h < 1) dst_h = 1
// Use nearest filter for retro look
return resize_mod.resize(img, dst_w, dst_h, { filter: "nearest" })
}
// Create a texture with platform-appropriate sizing, storing original for re-resize
function create_texture_for_platform(w, h, pixels, style, tier) {
var original = { width: w, height: h, pixels: pixels }
var img = resize_image_for_platform(original, style, tier)
var tex = _backend.create_texture(img.width, img.height, img.pixels)
// Tag texture with current style and tier for cache invalidation
tex._style_name = style ? style.name : null
tex._tier = tier || "normal"
tex[TEX_ORIGINAL] = original
return tex
}
// Get or create resized texture for current platform
function get_platform_texture(tex, style, tier) {
if (!tex) return _backend.get_white_texture()
// Check if texture needs re-resizing (style changed)
var style_name = style ? style.name : null
if (tex._style_name != style_name || tex._tier != tier) {
var original = tex[TEX_ORIGINAL]
if (original) {
var img = resize_image_for_platform(original, style, tier)
var new_tex = _backend.create_texture(img.width, img.height, img.pixels)
new_tex._style_name = style_name
new_tex._tier = tier
new_tex[TEX_ORIGINAL] = original
return new_tex
}
}
return tex
}
function load_texture(path, style, tier) {
var data = io.slurp(path)
if (!data) return null
var img = png.decode(data)
if (!img) return null
if (style) {
return create_texture_for_platform(img.width, img.height, img.pixels, style, tier)
} else {
return _backend.create_texture(img.width, img.height, img.pixels)
}
}
function load_model(path, style, tier) {
var ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase()
if (ext != "gltf" && ext != "glb") {
log.console("resources: unsupported model format: " + ext)
return null
}
var g = gltf.load(path, {pull_images: true, decode_images: true, mode: "used"})
if (!g) return null
var buffer_blob = g.buffers[0] ? g.buffers[0].blob : null
if (!buffer_blob) {
log.console("resources: gltf has no buffer data")
return null
}
var result = []
var textures = []
var materials = []
var original_images = []
// Load textures with platform-appropriate sizing
for (var ti = 0; ti < g.images.length; ti++) {
var img = g.images[ti]
var tex = null
if (img && img.pixels) {
original_images.push({
width: img.pixels.width,
height: img.pixels.height,
pixels: img.pixels.pixels
})
if (style) {
tex = create_texture_for_platform(img.pixels.width, img.pixels.height, img.pixels.pixels, style, tier)
} else {
tex = _backend.create_texture(img.pixels.width, img.pixels.height, img.pixels.pixels)
}
} else {
original_images.push(null)
}
textures.push(tex)
}
// Create materials
var gltf_mats = g.materials || []
for (var mi = 0; mi < gltf_mats.length; mi++) {
var gmat = gltf_mats[mi]
var paint = [1, 1, 1, 1]
if (gmat.pbr && gmat.pbr.base_color_factor) {
paint = gmat.pbr.base_color_factor.slice()
}
var color_map = null
if (gmat.pbr && gmat.pbr.base_color_texture) {
var tex_info = gmat.pbr.base_color_texture
var tex_obj = g.textures[tex_info.texture]
if (tex_obj && tex_obj.image != null && textures[tex_obj.image]) {
color_map = textures[tex_obj.image]
}
}
var coverage = "opaque"
if (gmat.alpha_mode == "MASK") coverage = "cutoff"
else if (gmat.alpha_mode == "BLEND") coverage = "blend"
materials.push({
color_map: color_map,
paint: paint,
coverage: coverage,
face: gmat.double_sided ? "double" : "single",
lamp: gmat.unlit ? "unlit" : "lit"
})
}
// Build internal model structure for animations/skins
var internal_model = {
meshes: [],
nodes: [],
root_nodes: [],
textures: textures,
materials: materials,
animations: [],
skins: [],
_gltf: g,
_tex_tier: tier || "normal",
_original_images: original_images
}
// Build node transforms
for (var ni = 0; ni < g.nodes.length; ni++) {
var node = g.nodes[ni]
var t = _make_internal_transform(node)
t.mesh_index = node.mesh
t.name = node.name
t.gltf_children = node.children || []
internal_model.nodes.push(t)
}
// Set up parent-child relationships
for (var ni = 0; ni < internal_model.nodes.length; ni++) {
var t = internal_model.nodes[ni]
for (var ci = 0; ci < t.gltf_children.length; ci++) {
var child_idx = t.gltf_children[ci]
if (child_idx < internal_model.nodes.length) {
_transform_set_parent(internal_model.nodes[child_idx], t)
}
}
}
// Find root nodes
for (var ni = 0; ni < internal_model.nodes.length; ni++) {
if (!internal_model.nodes[ni].parent) {
internal_model.root_nodes.push(internal_model.nodes[ni])
}
}
// Process meshes
for (var mi = 0; mi < g.meshes.length; mi++) {
var mesh = g.meshes[mi]
for (var pi = 0; pi < mesh.primitives.length; pi++) {
var prim = mesh.primitives[pi]
var gpu_mesh = _process_gltf_primitive(g, buffer_blob, prim, textures)
if (gpu_mesh) {
gpu_mesh.name = mesh.name
gpu_mesh.mesh_index = mi
gpu_mesh.primitive_index = pi
internal_model.meshes.push(gpu_mesh)
}
}
}
// Prepare animations and skins
internal_model.animations = anim_mod.prepare_animations(internal_model)
internal_model.skins = skin_mod.prepare_skins(internal_model)
// Build result array: [{mesh, material, node_index}]
for (var ni = 0; ni < internal_model.nodes.length; ni++) {
var node = internal_model.nodes[ni]
if (node.mesh_index == null) continue
for (var mi = 0; mi < internal_model.meshes.length; mi++) {
var mesh = internal_model.meshes[mi]
if (mesh.mesh_index != node.mesh_index) continue
var mat_idx = mesh.material_index
var mat = (mat_idx != null && materials[mat_idx]) ? materials[mat_idx] : meme(_default_material)
result.push({
mesh: mesh,
material: mat,
_node_index: ni
})
}
}
// Attach internal model for animation support
result._internal = internal_model
return result
}
// Recalculate model textures for a new style
function recalc_model_textures(model, style, tier) {
if (!model || !model._internal) return
var internal = model._internal
for (var ti = 0; ti < internal._original_images.length; ti++) {
var original = internal._original_images[ti]
if (!original) continue
var new_tex = create_texture_for_platform(original.width, original.height, original.pixels, style, tier)
internal.textures[ti] = new_tex
}
// Update material references
var g = internal._gltf
var gltf_mats = g.materials || []
for (var mi = 0; mi < gltf_mats.length; mi++) {
var gmat = gltf_mats[mi]
if (gmat.pbr && gmat.pbr.base_color_texture) {
var tex_info = gmat.pbr.base_color_texture
var tex_obj = g.textures[tex_info.texture]
if (tex_obj && tex_obj.image != null && internal.textures[tex_obj.image]) {
internal.materials[mi].color_map = internal.textures[tex_obj.image]
}
}
}
// Update mesh textures
for (var mi = 0; mi < internal.meshes.length; mi++) {
var mesh = internal.meshes[mi]
var mat_idx = mesh.material_index
if (mat_idx != null && internal.materials[mat_idx]) {
mesh.texture = internal.materials[mat_idx].color_map
}
}
// Update result array materials
for (var i = 0; i < model.length; i++) {
var entry = model[i]
var mat_idx = entry.mesh.material_index
if (mat_idx != null && internal.materials[mat_idx]) {
entry.material = internal.materials[mat_idx]
}
}
}
// Internal transform helpers
function _make_internal_transform(node) {
var t = {
parent: null,
children: [],
x: 0, y: 0, z: 0,
qx: 0, qy: 0, qz: 0, qw: 1,
sx: 1, sy: 1, sz: 1,
local_mat: null,
world_mat: null,
has_local_mat: false,
dirty_local: true,
dirty_world: true
}
if (node.matrix) {
t.local_mat = model_c.mat4_from_array(node.matrix)
t.has_local_mat = true
} else {
var trans = node.translation || [0, 0, 0]
var rot = node.rotation || [0, 0, 0, 1]
var scale = node.scale || [1, 1, 1]
t.x = trans[0]; t.y = trans[1]; t.z = trans[2]
t.qx = rot[0]; t.qy = rot[1]; t.qz = rot[2]; t.qw = rot[3]
t.sx = scale[0]; t.sy = scale[1]; t.sz = scale[2]
}
return t
}
function _transform_set_parent(child, parent) {
if (child.parent) {
var idx = child.parent.children.indexOf(child)
if (idx >= 0) child.parent.children.splice(idx, 1)
}
child.parent = parent
if (parent) parent.children.push(child)
child.dirty_world = true
}
function _transform_get_local_matrix(t) {
if (!t.dirty_local && t.local_mat) return t.local_mat
if (t.has_local_mat && t.local_mat) {
t.dirty_local = false
return t.local_mat
}
t.local_mat = model_c.mat4_from_trs(
t.x, t.y, t.z,
t.qx, t.qy, t.qz, t.qw,
t.sx, t.sy, t.sz
)
t.dirty_local = false
return t.local_mat
}
function _transform_get_world_matrix(t) {
if (!t.dirty_world && t.world_mat) return t.world_mat
var local = _transform_get_local_matrix(t)
if (t.parent) {
var parent_world = _transform_get_world_matrix(t.parent)
t.world_mat = model_c.mat4_mul(parent_world, local)
} else {
t.world_mat = local
}
t.dirty_world = false
return t.world_mat
}
function _process_gltf_primitive(g, buffer_blob, prim, textures) {
var attrs = prim.attributes
if (attrs.POSITION == null) return null
var pos_acc = g.accessors[attrs.POSITION]
var norm_acc = attrs.NORMAL != null ? g.accessors[attrs.NORMAL] : null
var uv_acc = attrs.TEXCOORD_0 != null ? g.accessors[attrs.TEXCOORD_0] : null
var color_acc = attrs.COLOR_0 != null ? g.accessors[attrs.COLOR_0] : null
var joints_acc = attrs.JOINTS_0 != null ? g.accessors[attrs.JOINTS_0] : null
var weights_acc = attrs.WEIGHTS_0 != null ? g.accessors[attrs.WEIGHTS_0] : null
var idx_acc = prim.indices != null ? g.accessors[prim.indices] : null
var vertex_count = pos_acc.count
var pos_view = g.views[pos_acc.view]
var positions = model_c.extract_accessor(
buffer_blob,
pos_view.byte_offset || 0,
pos_view.byte_stride || 0,
pos_acc.byte_offset || 0,
pos_acc.count,
pos_acc.component_type,
pos_acc.type
)
var normals = null
if (norm_acc) {
var norm_view = g.views[norm_acc.view]
normals = model_c.extract_accessor(
buffer_blob,
norm_view.byte_offset || 0,
norm_view.byte_stride || 0,
norm_acc.byte_offset || 0,
norm_acc.count,
norm_acc.component_type,
norm_acc.type
)
}
var uvs = null
if (uv_acc) {
var uv_view = g.views[uv_acc.view]
uvs = model_c.extract_accessor(
buffer_blob,
uv_view.byte_offset || 0,
uv_view.byte_stride || 0,
uv_acc.byte_offset || 0,
uv_acc.count,
uv_acc.component_type,
uv_acc.type
)
}
var colors = null
if (color_acc) {
var color_view = g.views[color_acc.view]
colors = model_c.extract_accessor(
buffer_blob,
color_view.byte_offset || 0,
color_view.byte_stride || 0,
color_acc.byte_offset || 0,
color_acc.count,
color_acc.component_type,
color_acc.type
)
}
var joints = null
if (joints_acc) {
var joints_view = g.views[joints_acc.view]
joints = model_c.extract_accessor(
buffer_blob,
joints_view.byte_offset || 0,
joints_view.byte_stride || 0,
joints_acc.byte_offset || 0,
joints_acc.count,
joints_acc.component_type,
joints_acc.type
)
}
var weights = null
if (weights_acc) {
var weights_view = g.views[weights_acc.view]
weights = model_c.extract_accessor(
buffer_blob,
weights_view.byte_offset || 0,
weights_view.byte_stride || 0,
weights_acc.byte_offset || 0,
weights_acc.count,
weights_acc.component_type,
weights_acc.type
)
}
var indices = null
var index_count = 0
var index_type = "uint16"
if (idx_acc) {
var idx_view = g.views[idx_acc.view]
indices = model_c.extract_indices(
buffer_blob,
idx_view.byte_offset || 0,
idx_acc.byte_offset || 0,
idx_acc.count,
idx_acc.component_type
)
index_count = idx_acc.count
index_type = idx_acc.component_type == "u32" ? "uint32" : "uint16"
}
var mesh_data = {
vertex_count: vertex_count,
positions: positions,
normals: normals,
uvs: uvs,
colors: colors,
joints: joints,
weights: weights
}
var packed = model_c.pack_vertices(mesh_data)
var vertex_buffer = _backend.create_vertex_buffer(packed.data)
var index_buffer = indices ? _backend.create_index_buffer(indices) : null
var texture = null
if (prim.material != null && g.materials[prim.material]) {
var mat = g.materials[prim.material]
if (mat.pbr && mat.pbr.base_color_texture) {
var tex_info = mat.pbr.base_color_texture
var tex_obj = g.textures[tex_info.texture]
if (tex_obj && tex_obj.image != null && textures[tex_obj.image]) {
texture = textures[tex_obj.image]
}
}
}
return {
vertex_buffer: vertex_buffer,
index_buffer: index_buffer,
index_count: index_count,
index_type: index_type,
vertex_count: vertex_count,
material_index: prim.material,
texture: texture,
skinned: packed.skinned || false,
stride: packed.stride
}
}
// Export transform helpers for use by other modules
function get_transform_world_matrix(t) {
return _transform_get_world_matrix(t)
}
return {
set_backend: set_backend,
load_texture: load_texture,
load_model: load_model,
recalc_model_textures: recalc_model_textures,
create_texture_for_platform: create_texture_for_platform,
get_platform_texture: get_platform_texture,
get_transform_world_matrix: get_transform_world_matrix,
default_material: _default_material
}