load gltf correctly

This commit is contained in:
2025-12-13 16:13:13 -06:00
parent 1c1fc8fe95
commit d4eee53926
6 changed files with 379 additions and 177 deletions

401
core.cm
View File

@@ -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,

View File

@@ -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

67
model.c
View File

@@ -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));
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)