// 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 = "resources: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 / max(src_w, src_h) var dst_w = floor(src_w * scale) var dst_h = 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 = lower(text(path, path.lastIndexOf('.') + 1)) 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 < length(g.images); 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 < length(gltf_mats); mi++) { var gmat = gltf_mats[mi] var paint = [1, 1, 1, 1] if (gmat.pbr && gmat.pbr.base_color_factor) { paint = array(gmat.pbr.base_color_factor) } 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 < length(g.nodes); 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 < length(internal_model.nodes); ni++) { var t = internal_model.nodes[ni] for (var ci = 0; ci < length(t.gltf_children); ci++) { var child_idx = t.gltf_children[ci] if (child_idx < length(internal_model.nodes)) { _transform_set_parent(internal_model.nodes[child_idx], t) } } } // Find root nodes for (var ni = 0; ni < length(internal_model.nodes); ni++) { if (!internal_model.nodes[ni].parent) { internal_model.root_nodes.push(internal_model.nodes[ni]) } } // Process meshes for (var mi = 0; mi < length(g.meshes); mi++) { var mesh = g.meshes[mi] for (var pi = 0; pi < length(mesh.primitives); 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 < length(internal_model.nodes); ni++) { var node = internal_model.nodes[ni] if (node.mesh_index == null) continue for (var mi = 0; mi < length(internal_model.meshes); 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 < length(internal._original_images); 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 < length(gltf_mats); 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 < length(internal.meshes); 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 < length(model); 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 = find(child.parent.children, child) if (idx != null) 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 }