load gltf correctly
This commit is contained in:
401
core.cm
401
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,
|
||||
|
||||
@@ -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
67
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user