553 lines
15 KiB
Plaintext
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 = "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
|
|
}
|