diff --git a/core.cm b/core.cm index d34823b..eb66238 100644 --- a/core.cm +++ b/core.cm @@ -10,7 +10,6 @@ var gltf = use('mload/gltf') var obj_loader = use('mload/obj') var model_c = use('model') var png = use('cell-image/png') -var jpg = use('cell-image/jpg') var anim_mod = use('animation') var skin_mod = use('skin') @@ -29,13 +28,18 @@ var _state = { // GPU resources window: null, gpu: null, - pipeline_lit: null, - pipeline_unlit: null, - pipeline_skinned: null, + // Pipelines for different alpha modes and culling + // Key format: "skinned_alphamode_cull" e.g. "false_opaque_back", "true_blend_none" + pipelines: {}, sampler_nearest: null, sampler_linear: null, depth_texture: null, white_texture: null, + // Shader references for pipeline creation + vert_shader: null, + frag_shader: null, + skinned_vert_shader: null, + swapchain_format: null, // Camera state camera: { @@ -169,12 +173,11 @@ function log_msg() { // ============================================================================ function load_model(path) { - var data = io.slurp(path) - if (!data) return null - var ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase() if (ext == "obj") { + var data = io.slurp(path) + if (!data) return null var parsed = obj_loader.decode(data) if (!parsed) return null return _load_obj_model(parsed) @@ -185,8 +188,8 @@ function load_model(path) { return null } - // Parse gltf - var g = gltf.decode(data) + // Parse gltf + pull/decode used images + var g = gltf.load(path, {pull_images:true, decode_images:true, mode:"used"}) if (!g) return null // Get the main buffer blob @@ -202,35 +205,64 @@ function load_model(path) { nodes: [], root_nodes: [], textures: [], - materials: g.materials || [], + materials: [], animations: [], animation_count: g.animations ? g.animations.length : 0, skins: [], _gltf: g } - // Load textures from embedded images + // Load textures from decoded gltf images for (var ti = 0; ti < g.images.length; ti++) { var img = g.images[ti] var tex = null - if (img.kind == "buffer_view" && img.view != null) { - var view = g.views[img.view] - var img_data = _extract_buffer_view(buffer_blob, view) - if (img.mime == "image/png") { - var decoded = png.decode(img_data) - if (decoded) { - tex = _create_texture(decoded.width, decoded.height, decoded.pixels) - } - } else if (img.mime == "image/jpeg") { - var decoded = jpg.decode(img_data) - if (decoded) { - tex = _create_texture(decoded.width, decoded.height, decoded.pixels) - } - } + if (img && img.pixels) { + tex = _create_texture(img.pixels.width, img.pixels.height, img.pixels.pixels) } model.textures.push(tex) } + // Create materials from glTF material data + var gltf_mats = g.materials || [] + for (var mi = 0; mi < gltf_mats.length; mi++) { + var gmat = gltf_mats[mi] + + // Get base color factor (default white) + var base_color = [1, 1, 1, 1] + if (gmat.pbr && gmat.pbr.base_color_factor) { + base_color = gmat.pbr.base_color_factor.slice() + } + + // Get texture if present + var tex = 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 && model.textures[tex_obj.image]) { + tex = model.textures[tex_obj.image] + } + } + + // Convert alpha_mode string to our format + var alpha_mode = "opaque" + if (gmat.alpha_mode == "MASK") alpha_mode = "mask" + else if (gmat.alpha_mode == "BLEND") alpha_mode = "blend" + + // Check for unlit extension (KHR_materials_unlit) + var is_unlit = gmat.unlit || false + + var mat = make_material(is_unlit ? "unlit" : "lit", { + texture: tex, + color: base_color, + alpha_mode: alpha_mode, + alpha_cutoff: gmat.alpha_cutoff != null ? gmat.alpha_cutoff : 0.5, + double_sided: gmat.double_sided || false, + unlit: is_unlit + }) + mat.name = gmat.name + model.materials.push(mat) + } + // Build node transforms (preserving hierarchy) for (var ni = 0; ni < g.nodes.length; ni++) { var node = g.nodes[ni] @@ -300,22 +332,15 @@ function load_model(path) { return model } -function _extract_buffer_view(buffer_blob, view) { - // Extract a portion of the buffer as a new blob - var offset = view.byte_offset || 0 - var length = view.byte_length - var newblob = buffer_blob.read_blob(offset*8, (offset + length)*8) - return stone(newblob) -} - function _process_gltf_primitive(g, buffer_blob, prim, textures) { var attrs = prim.attributes - if (!attrs.POSITION) return null + if (attrs.POSITION == null) return null // Get accessors 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 @@ -364,6 +389,21 @@ function _process_gltf_primitive(g, buffer_blob, prim, textures) { ) } + // Extract vertex colors (COLOR_0) + 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 + ) + } + // Extract joints (for skinned meshes) var joints = null if (joints_acc) { @@ -417,7 +457,7 @@ function _process_gltf_primitive(g, buffer_blob, prim, textures) { positions: positions, normals: normals, uvs: uvs, - colors: null, + colors: colors, joints: joints, weights: weights } @@ -852,19 +892,38 @@ function pop_state() { // 6) Materials, Lighting, Fog // ============================================================================ +// Uber material - base prototype for all materials with sane defaults +var _uber_material = { + kind: "lit", // "lit" or "unlit" + texture: null, + color: [1, 1, 1, 1], // base_color_factor (RGBA) + alpha_mode: "opaque", // "opaque", "mask", or "blend" + alpha_cutoff: 0.5, // for mask mode + double_sided: false, + unlit: false +} + function make_material(kind, opts) { opts = opts || {} - return { - kind: kind, - texture: opts.texture || null, - color: opts.color || [1, 1, 1, 1] - } + var mat = Object.create(_uber_material) + mat.kind = kind || "lit" + mat.texture = opts.texture || null + mat.color = opts.color || [1, 1, 1, 1] + mat.alpha_mode = opts.alpha_mode || "opaque" + mat.alpha_cutoff = opts.alpha_cutoff != null ? opts.alpha_cutoff : 0.5 + mat.double_sided = opts.double_sided || false + mat.unlit = kind == "unlit" || opts.unlit || false + return mat } function set_material(material) { _state.current_material = material } +function get_material() { + return _state.current_material +} + function set_ambient(r, g, b) { _state.ambient = [r, g, b] } @@ -952,13 +1011,23 @@ function draw_model(model, transform, anim_instance) { ? model_c.mat4_mul(extra_transform, node_world) : node_world - // Get material/texture + // Determine which material to use: + // 1. If current_material is set, use it (user override) + // 2. Otherwise use the mesh's material from the model + // 3. Fall back to uber material defaults var mat = _state.current_material - var tint = mat ? mat.color : [1, 1, 1, 1] - var tex = mesh.texture || (mat && mat.texture ? mat.texture : _state.white_texture) + if (!mat && mesh.material_index != null && model.materials[mesh.material_index]) { + mat = model.materials[mesh.material_index] + } + if (!mat) { + mat = _uber_material + } - // Build uniforms in cell script - var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, tint) + // Get texture: prefer mesh texture, then material texture, then white + var tex = mesh.texture || mat.texture || _state.white_texture + + // Build uniforms with material properties + var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, mat) // Get palette for skinned mesh (use first skin for now) var palette = null @@ -966,7 +1035,7 @@ function draw_model(model, transform, anim_instance) { palette = skin_palettes[0] } - _draw_mesh(mesh, uniforms, tex, mat ? mat.kind : "lit", palette) + _draw_mesh(mesh, uniforms, tex, mat, palette) _state.draw_calls++ _state.triangles += mesh.index_count / 3 } @@ -975,13 +1044,21 @@ function draw_model(model, transform, anim_instance) { // If model has no nodes with meshes, draw meshes directly (fallback) if (model.nodes.length == 0) { var world_matrix = transform ? transform_get_world_matrix(transform) : model_c.mat4_identity() - var mat = _state.current_material - var tint = mat ? mat.color : [1, 1, 1, 1] for (var i = 0; i < model.meshes.length; i++) { var mesh = model.meshes[i] - var tex = mesh.texture || (mat && mat.texture ? mat.texture : _state.white_texture) - var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, tint) + + // Determine material + var mat = _state.current_material + if (!mat && mesh.material_index != null && model.materials[mesh.material_index]) { + mat = model.materials[mesh.material_index] + } + if (!mat) { + mat = _uber_material + } + + var tex = mesh.texture || mat.texture || _state.white_texture + var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, mat) // Get palette for skinned mesh var palette = null @@ -989,7 +1066,7 @@ function draw_model(model, transform, anim_instance) { palette = skin_palettes[0] } - _draw_mesh(mesh, uniforms, tex, mat ? mat.kind : "lit", palette) + _draw_mesh(mesh, uniforms, tex, mat, palette) _state.draw_calls++ _state.triangles += mesh.index_count / 3 } @@ -997,7 +1074,24 @@ function draw_model(model, transform, anim_instance) { } // Build uniform buffer using C helper (blob API doesn't have write_f32) -function _build_uniforms(model_mat, view_mat, proj_mat, tint) { +// mat can be a material object or a tint array for backwards compatibility +function _build_uniforms(model_mat, view_mat, proj_mat, mat) { + // Handle backwards compatibility: if mat is an array, treat as tint + var tint = [1, 1, 1, 1] + var alpha_mode = 0 // 0=opaque, 1=mask, 2=blend + var alpha_cutoff = 0.5 + var unlit = 0 + + if (Array.isArray(mat)) { + tint = mat + } else if (mat) { + tint = mat.color || [1, 1, 1, 1] + if (mat.alpha_mode == "mask") alpha_mode = 1 + else if (mat.alpha_mode == "blend") alpha_mode = 2 + alpha_cutoff = mat.alpha_cutoff != null ? mat.alpha_cutoff : 0.5 + unlit = (mat.unlit || mat.kind == "unlit") ? 1 : 0 + } + return model_c.build_uniforms({ model: model_mat, view: view_mat, @@ -1012,7 +1106,10 @@ function _build_uniforms(model_mat, view_mat, proj_mat, tint) { tint: tint, style_id: _state.style_id, resolution_w: _state.resolution_w, - resolution_h: _state.resolution_h + resolution_h: _state.resolution_h, + alpha_mode: alpha_mode, + alpha_cutoff: alpha_cutoff, + unlit: unlit }) } @@ -1250,6 +1347,75 @@ function irand(min_inclusive, max_inclusive) { // Internal GPU functions // ============================================================================ +// Get or create a pipeline for the given configuration +// skinned: bool, alpha_mode: "opaque"|"mask"|"blend", cull: "back"|"front"|"none" +function _get_pipeline(skinned, alpha_mode, cull) { + var key = `${skinned}_${alpha_mode}_${cull}` + if (_state.pipelines[key]) return _state.pipelines[key] + + // Determine blend settings based on alpha mode + var blend_enabled = alpha_mode == "blend" + var depth_write = alpha_mode != "blend" // blend mode typically doesn't write depth + + var blend_config = { enabled: false } + if (blend_enabled) { + blend_config = { + enabled: true, + src_rgb: "src_alpha", + dst_rgb: "one_minus_src_alpha", + op_rgb: "add", + src_alpha: "one", + dst_alpha: "one_minus_src_alpha", + op_alpha: "add" + } + } + + // Determine cull mode + var cull_mode = cull == "none" ? "none" : (cull == "front" ? "front" : "back") + + var vert_shader = skinned ? _state.skinned_vert_shader : _state.vert_shader + if (!vert_shader) return null + + var pitch = skinned ? 80 : 48 + var vertex_attrs = [ + { location: 0, buffer_slot: 0, format: "float3", offset: 0 }, + { location: 1, buffer_slot: 0, format: "float3", offset: 12 }, + { location: 2, buffer_slot: 0, format: "float2", offset: 24 }, + { location: 3, buffer_slot: 0, format: "float4", offset: 32 } + ] + if (skinned) { + vertex_attrs.push({ location: 4, buffer_slot: 0, format: "float4", offset: 48 }) + vertex_attrs.push({ location: 5, buffer_slot: 0, format: "float4", offset: 64 }) + } + + var pipeline = new gpu_mod.graphics_pipeline(_state.gpu, { + vertex: vert_shader, + fragment: _state.frag_shader, + primitive: "triangle", + cull: cull_mode, + face: "counter_clockwise", + fill: "fill", + vertex_buffer_descriptions: [{ + slot: 0, + pitch: pitch, + input_rate: "vertex" + }], + vertex_attributes: vertex_attrs, + target: { + color_targets: [{ format: _state.swapchain_format, blend: blend_config }], + depth: "d32 float s8" + }, + depth: { + test: true, + write: depth_write, + compare: "less" + } + }) + + _state.pipelines[key] = pipeline + return pipeline +} + function _init_gpu() { // Create window _state.window = new video.window({ @@ -1271,7 +1437,7 @@ function _init_gpu() { return } - var vert_shader = new gpu_mod.shader(_state.gpu, { + _state.vert_shader = new gpu_mod.shader(_state.gpu, { code: vert_code, stage: "vertex", format: "msl", @@ -1279,7 +1445,7 @@ function _init_gpu() { num_uniform_buffers: 2 }) - var frag_shader = new gpu_mod.shader(_state.gpu, { + _state.frag_shader = new gpu_mod.shader(_state.gpu, { code: frag_code, stage: "fragment", format: "msl", @@ -1288,80 +1454,24 @@ function _init_gpu() { num_samplers: 1 }) - // Create pipeline - var swapchain_format = _state.gpu.swapchain_format(_state.window) - - _state.pipeline_lit = new gpu_mod.graphics_pipeline(_state.gpu, { - vertex: vert_shader, - fragment: frag_shader, - primitive: "triangle", - cull: "back", - face: "counter_clockwise", - fill: "fill", - vertex_buffer_descriptions: [{ - slot: 0, - pitch: 48, - input_rate: "vertex" - }], - vertex_attributes: [ - { location: 0, buffer_slot: 0, format: "float3", offset: 0 }, - { location: 1, buffer_slot: 0, format: "float3", offset: 12 }, - { location: 2, buffer_slot: 0, format: "float2", offset: 24 }, - { location: 3, buffer_slot: 0, format: "float4", offset: 32 } - ], - target: { - color_targets: [{ format: swapchain_format, blend: { enabled: false } }], - depth: "d32 float s8" - }, - depth: { - test: true, - write: true, - compare: "less" - } - }) + // Store swapchain format for pipeline creation + _state.swapchain_format = _state.gpu.swapchain_format(_state.window) - // Create skinned pipeline (for animated meshes with joints/weights) + // Load skinned vertex shader var skinned_vert_code = io.slurp("shaders/retro3d_skinned.vert.msl") if (skinned_vert_code) { - var skinned_vert_shader = new gpu_mod.shader(_state.gpu, { + _state.skinned_vert_shader = new gpu_mod.shader(_state.gpu, { code: skinned_vert_code, stage: "vertex", format: "msl", entrypoint: "vertex_main", num_uniform_buffers: 3 }) - - _state.pipeline_skinned = new gpu_mod.graphics_pipeline(_state.gpu, { - vertex: skinned_vert_shader, - fragment: frag_shader, - primitive: "triangle", - cull: "back", - face: "counter_clockwise", - fill: "fill", - vertex_buffer_descriptions: [{ - slot: 0, - pitch: 80, - input_rate: "vertex" - }], - vertex_attributes: [ - { location: 0, buffer_slot: 0, format: "float3", offset: 0 }, - { location: 1, buffer_slot: 0, format: "float3", offset: 12 }, - { location: 2, buffer_slot: 0, format: "float2", offset: 24 }, - { location: 3, buffer_slot: 0, format: "float4", offset: 32 }, - { location: 4, buffer_slot: 0, format: "float4", offset: 48 }, - { location: 5, buffer_slot: 0, format: "float4", offset: 64 } - ], - target: { - color_targets: [{ format: swapchain_format, blend: { enabled: false } }], - depth: "d32 float s8" - }, - depth: { - test: true, - write: true, - compare: "less" - } - }) } + + // Create default pipelines (opaque, back-face culling) + _get_pipeline(false, "opaque", "back") + _get_pipeline(true, "opaque", "back") // Create samplers var style = _styles[_state.style] @@ -1543,14 +1653,24 @@ function _compute_projection_matrix() { } } -function _draw_mesh(mesh, uniforms, texture, kind, palette) { +function _draw_mesh(mesh, uniforms, texture, mat, palette) { // This will be called during render pass _state._pending_draws = _state._pending_draws || [] + + // Extract material properties for pipeline selection + var alpha_mode = "opaque" + var double_sided = false + if (mat && typeof mat == "object" && !Array.isArray(mat)) { + alpha_mode = mat.alpha_mode || "opaque" + double_sided = mat.double_sided || false + } + _state._pending_draws.push({ mesh: mesh, uniforms: uniforms, texture: texture, - kind: kind, + alpha_mode: alpha_mode, + double_sided: double_sided, palette: palette }) } @@ -1653,28 +1773,34 @@ function _end_frame() { var swap_pass = cmd.swapchain_pass(_state.window, pass_desc) // Draw all pending meshes + // Sort by alpha mode: opaque first, then mask, then blend (for proper transparency) var draws = _state._pending_draws || [] + draws.sort(function(a, b) { + var order = { opaque: 0, mask: 1, blend: 2 } + return (order[a.alpha_mode] || 0) - (order[b.alpha_mode] || 0) + }) + for (var i = 0; i < draws.length; i++) { var d = draws[i] - // Choose pipeline based on whether mesh is skinned - if (d.mesh.skinned && d.palette && _state.pipeline_skinned) { - swap_pass.bind_pipeline(_state.pipeline_skinned) - swap_pass.bind_vertex_buffers(0, [{ buffer: d.mesh.vertex_buffer, offset: 0 }]) - swap_pass.bind_index_buffer({ buffer: d.mesh.index_buffer, offset: 0 }, d.mesh.index_type == "uint32" ? 32 : 16) + // Select pipeline based on skinned, alpha_mode, and double_sided + var skinned = d.mesh.skinned && d.palette + var cull = d.double_sided ? "none" : "back" + var pipeline = _get_pipeline(skinned, d.alpha_mode, cull) - // Shaders use [[buffer(1)]] for uniforms, [[buffer(2)]] for joint palette - cmd.push_vertex_uniform_data(1, d.uniforms) + if (!pipeline) continue + + swap_pass.bind_pipeline(pipeline) + swap_pass.bind_vertex_buffers(0, [{ buffer: d.mesh.vertex_buffer, offset: 0 }]) + swap_pass.bind_index_buffer({ buffer: d.mesh.index_buffer, offset: 0 }, d.mesh.index_type == "uint32" ? 32 : 16) + + // Push uniforms + cmd.push_vertex_uniform_data(1, d.uniforms) + cmd.push_fragment_uniform_data(1, d.uniforms) + + // Push joint palette for skinned meshes + if (skinned && d.palette) { cmd.push_vertex_uniform_data(2, d.palette) - cmd.push_fragment_uniform_data(1, d.uniforms) - } else { - swap_pass.bind_pipeline(_state.pipeline_lit) - swap_pass.bind_vertex_buffers(0, [{ buffer: d.mesh.vertex_buffer, offset: 0 }]) - swap_pass.bind_index_buffer({ buffer: d.mesh.index_buffer, offset: 0 }, d.mesh.index_type == "uint32" ? 32 : 16) - - // Shaders use [[buffer(1)]] for uniforms (buffer(0) is vertex data) - cmd.push_vertex_uniform_data(1, d.uniforms) - cmd.push_fragment_uniform_data(1, d.uniforms) } var sampler = _state.style_id == 1 ? _state.sampler_linear : _state.sampler_nearest @@ -1735,6 +1861,7 @@ return { make_material: make_material, set_material: set_material, + get_material: get_material, set_ambient: set_ambient, set_light_dir: set_light_dir, set_fog: set_fog, diff --git a/examples/modelview.ce b/examples/modelview.ce index 7c0a58b..c751d0c 100644 --- a/examples/modelview.ce +++ b/examples/modelview.ce @@ -66,12 +66,6 @@ function _init() { retro3d.set_ambient(0.3, 0.3, 0.35) retro3d.set_light_dir(0.5, 1.0, 0.3, 1.0, 0.95, 0.9, 1.0) - // Set up a default material - var mat = retro3d.make_material("lit", { - color: [1, 1, 1, 1] - }) - retro3d.set_material(mat) - last_time = time_mod.number() log.console("") @@ -185,7 +179,7 @@ function _draw() { if (model) { retro3d.draw_model(model, transform) } - + return // Draw a ground grid using immediate mode retro3d.push_state() var grid_mat = retro3d.make_material("unlit", { @@ -209,6 +203,8 @@ function _draw() { retro3d.end() retro3d.pop_state() + + } function frame() { @@ -237,7 +233,7 @@ function frame() { retro3d._end_frame() // Schedule next frame - $_.delay(frame, 1/60) + $_.delay(frame, 1/240) } // Start diff --git a/model.c b/model.c index 14f0f23..2b61af3 100644 --- a/model.c +++ b/model.c @@ -473,6 +473,18 @@ JSValue js_model_pack_vertices(JSContext *js, JSValue this_val, int argc, JSValu size_t total_size = vertex_count * stride; float *packed = malloc(total_size); + // Detect if colors are vec3 (RGB) or vec4 (RGBA) + // vec3: color_size = vertex_count * 3 * sizeof(float) + // vec4: color_size = vertex_count * 4 * sizeof(float) + int color_components = 4; + if (colors && color_size > 0) { + size_t expected_vec4 = (size_t)vertex_count * 4 * sizeof(float); + size_t expected_vec3 = (size_t)vertex_count * 3 * sizeof(float); + if (color_size == expected_vec3) { + color_components = 3; + } + } + for (int i = 0; i < vertex_count; i++) { float *v = &packed[i * floats_per_vertex]; @@ -498,12 +510,19 @@ JSValue js_model_pack_vertices(JSContext *js, JSValue this_val, int argc, JSValu v[6] = 0; v[7] = 0; } - // Color + // Color (handle both vec3 and vec4) if (colors) { - v[8] = colors[i * 4 + 0]; - v[9] = colors[i * 4 + 1]; - v[10] = colors[i * 4 + 2]; - v[11] = colors[i * 4 + 3]; + if (color_components == 4) { + v[8] = colors[i * 4 + 0]; + v[9] = colors[i * 4 + 1]; + v[10] = colors[i * 4 + 2]; + v[11] = colors[i * 4 + 3]; + } else { + v[8] = colors[i * 3 + 0]; + v[9] = colors[i * 3 + 1]; + v[10] = colors[i * 3 + 2]; + v[11] = 1.0f; // Default alpha for vec3 colors + } } else { v[8] = 1; v[9] = 1; v[10] = 1; v[11] = 1; } @@ -542,7 +561,7 @@ JSValue js_model_pack_vertices(JSContext *js, JSValue this_val, int argc, JSValu } // Build uniform buffer for retro3d rendering -// Layout matches shader struct Uniforms (384 bytes = 96 floats): +// Layout matches shader struct Uniforms (400 bytes = 100 floats): // float4x4 mvp [0-15] (64 bytes) // float4x4 model [16-31] (64 bytes) // float4x4 view [32-47] (64 bytes) @@ -552,17 +571,18 @@ JSValue js_model_pack_vertices(JSContext *js, JSValue this_val, int argc, JSValu // float4 light_color [72-75] (16 bytes) - rgb, intensity // float4 fog_params [76-79] (16 bytes) - near, far, unused, enabled // float4 fog_color [80-83] (16 bytes) - rgb, unused -// float4 tint [84-87] (16 bytes) - rgba +// float4 tint [84-87] (16 bytes) - rgba (base_color_factor) // float4 style_params [88-91] (16 bytes) - style_id, vertex_snap, affine, dither // float4 resolution [92-95] (16 bytes) - w, h, unused, unused +// float4 material_params [96-99] (16 bytes) - alpha_mode, alpha_cutoff, unlit, unused JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 1) return JS_ThrowTypeError(js, "build_uniforms requires params object"); JSValue params = argv[0]; - // Allocate uniform buffer (384 bytes = 96 floats) - float uniforms[96] = {0}; + // Allocate uniform buffer (400 bytes = 100 floats) + float uniforms[100] = {0}; // Get matrices JSValue model_v = JS_GetPropertyStr(js, params, "model"); @@ -717,6 +737,35 @@ JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSVal JS_FreeValue(js, res_w_v); JS_FreeValue(js, res_h_v); + // Material params at offset 96-99 (alpha_mode, alpha_cutoff, unlit, unused) + // alpha_mode: 0=OPAQUE, 1=MASK, 2=BLEND + JSValue alpha_mode_v = JS_GetPropertyStr(js, params, "alpha_mode"); + JSValue alpha_cutoff_v = JS_GetPropertyStr(js, params, "alpha_cutoff"); + JSValue unlit_v = JS_GetPropertyStr(js, params, "unlit"); + + double alpha_mode = 0.0; // default OPAQUE + double alpha_cutoff = 0.5; // glTF default + double unlit_d = 0.0; + + if (!JS_IsNull(alpha_mode_v)) { + JS_ToFloat64(js, &alpha_mode, alpha_mode_v); + } + if (!JS_IsNull(alpha_cutoff_v)) { + JS_ToFloat64(js, &alpha_cutoff, alpha_cutoff_v); + } + if (!JS_IsNull(unlit_v)) { + JS_ToFloat64(js, &unlit_d, unlit_v); + } + + uniforms[96] = (float)alpha_mode; + uniforms[97] = (float)alpha_cutoff; + uniforms[98] = (float)unlit_d; + uniforms[99] = 0.0f; // unused + + JS_FreeValue(js, alpha_mode_v); + JS_FreeValue(js, alpha_cutoff_v); + JS_FreeValue(js, unlit_v); + return js_new_blob_stoned_copy(js, uniforms, sizeof(uniforms)); } diff --git a/shaders/retro3d.frag.msl b/shaders/retro3d.frag.msl index fe707dd..6a3eac0 100644 --- a/shaders/retro3d.frag.msl +++ b/shaders/retro3d.frag.msl @@ -11,9 +11,10 @@ struct Uniforms { float4 light_color; // rgb, intensity float4 fog_params; // near, far, unused, enabled float4 fog_color; // rgb, unused - float4 tint; // rgba + float4 tint; // rgba (base_color_factor from glTF) float4 style_params; // style_id, vertex_snap, affine, dither float4 resolution; // w, h, unused, unused + float4 material_params; // alpha_mode (0=opaque,1=mask,2=blend), alpha_cutoff, unlit, unused }; struct FragmentIn { @@ -55,6 +56,11 @@ fragment float4 fragment_main( float affine = uniforms.style_params.z; float dither = uniforms.style_params.w; + // Material params + float alpha_mode = uniforms.material_params.x; // 0=opaque, 1=mask, 2=blend + float alpha_cutoff = uniforms.material_params.y; // default 0.5 + float unlit = uniforms.material_params.z; + // Get UV coordinates float2 uv; if (affine > 0.5) { @@ -67,48 +73,70 @@ fragment float4 fragment_main( // Sample texture float4 tex_color = tex.sample(samp, uv); - // Start with vertex color * texture - float4 base_color = in.color * tex_color; + // glTF spec: final color = vertexColor * baseColorFactor * baseColorTexture + // tint = base_color_factor from material + float4 base_color = in.color * uniforms.tint * tex_color; - // Lighting calculation - float3 normal = normalize(in.world_normal); - float3 light_dir = normalize(uniforms.light_dir.xyz); - float ndotl = max(dot(normal, light_dir), 0.0); + // Alpha handling based on alpha_mode + float alpha = base_color.a; - float3 ambient = uniforms.ambient.rgb; - float3 diffuse = uniforms.light_color.rgb * uniforms.light_color.w * ndotl; - float3 lighting = ambient + diffuse; + // MASK mode: discard fragments below cutoff + if (alpha_mode > 0.5 && alpha_mode < 1.5) { + if (alpha < alpha_cutoff) { + discard_fragment(); + } + alpha = 1.0; // MASK mode outputs fully opaque or discards + } - // Apply lighting - float3 lit_color = base_color.rgb * lighting; + // OPAQUE mode: ignore alpha entirely + if (alpha_mode < 0.5) { + alpha = 1.0; + } - // Apply tint - lit_color *= uniforms.tint.rgb; - float alpha = base_color.a * uniforms.tint.a; + // BLEND mode: alpha is used as-is (alpha_mode >= 1.5) + + float3 final_color; + + if (unlit > 0.5) { + // Unlit material - no lighting calculation + final_color = base_color.rgb; + } else { + // Lighting calculation + float3 normal = normalize(in.world_normal); + float3 light_dir = normalize(uniforms.light_dir.xyz); + float ndotl = max(dot(normal, light_dir), 0.0); + + float3 ambient = uniforms.ambient.rgb; + float3 diffuse = uniforms.light_color.rgb * uniforms.light_color.w * ndotl; + float3 lighting = ambient + diffuse; + + // Apply lighting + final_color = base_color.rgb * lighting; + } // Style-specific processing if (style_id < 0.5) { // PS1 style: 15-bit color (5 bits per channel) - lit_color = quantize_color(lit_color, 5.0); + final_color = quantize_color(final_color, 5.0); } else if (style_id < 1.5) { // N64 style: smoother, 16-bit color with bilinear filtering // (filtering is handled by sampler, just quantize slightly) - lit_color = quantize_color(lit_color, 5.0); + final_color = quantize_color(final_color, 5.0); } else { // Saturn style: dithered, flat shaded look if (dither > 0.5) { float d = get_dither(in.position.xy); // Add dither before quantization - lit_color += (d - 0.5) * 0.1; + final_color += (d - 0.5) * 0.1; } - lit_color = quantize_color(lit_color, 5.0); + final_color = quantize_color(final_color, 5.0); } // Apply fog float3 fog_color = uniforms.fog_color.rgb; - lit_color = mix(fog_color, lit_color, in.fog_factor); + final_color = mix(fog_color, final_color, in.fog_factor); - return float4(lit_color, alpha); + return float4(final_color, alpha); } // Unlit fragment shader for sprites/UI diff --git a/shaders/retro3d.vert.msl b/shaders/retro3d.vert.msl index 835d765..95a535a 100644 --- a/shaders/retro3d.vert.msl +++ b/shaders/retro3d.vert.msl @@ -11,9 +11,10 @@ struct Uniforms { float4 light_color; // rgb, intensity float4 fog_params; // near, far, unused, enabled float4 fog_color; // rgb, unused - float4 tint; // rgba + float4 tint; // rgba (base_color_factor from glTF) float4 style_params; // style_id, vertex_snap, affine, dither float4 resolution; // w, h, unused, unused + float4 material_params; // alpha_mode (0=opaque,1=mask,2=blend), alpha_cutoff, unlit, unused }; struct VertexIn { diff --git a/shaders/retro3d_skinned.vert.msl b/shaders/retro3d_skinned.vert.msl index 7ceffb5..4bb3284 100644 --- a/shaders/retro3d_skinned.vert.msl +++ b/shaders/retro3d_skinned.vert.msl @@ -11,9 +11,10 @@ struct Uniforms { float4 light_color; // rgb, intensity float4 fog_params; // near, far, unused, enabled float4 fog_color; // rgb, unused - float4 tint; // rgba + float4 tint; // rgba (base_color_factor from glTF) float4 style_params; // style_id, vertex_snap, affine, dither float4 resolution; // w, h, unused, unused + float4 material_params; // alpha_mode (0=opaque,1=mask,2=blend), alpha_cutoff, unlit, unused }; // Joint palette: up to 64 joints (64 * 64 bytes = 4096 bytes)