1790 lines
47 KiB
Plaintext
1790 lines
47 KiB
Plaintext
// retro3d fantasy game console
|
|
var io = use('fd')
|
|
var time_mod = use('time')
|
|
var blob_mod = use('blob')
|
|
var video = use('sdl3/video')
|
|
var gpu_mod = use('sdl3/gpu')
|
|
var events = use('sdl3/events')
|
|
var keyboard = use('sdl3/keyboard')
|
|
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')
|
|
|
|
// Internal state
|
|
var _state = {
|
|
style: null,
|
|
style_id: 0, // 0=ps1, 1=n64, 2=saturn
|
|
resolution_w: 320,
|
|
resolution_h: 240,
|
|
boot_time: 0,
|
|
dt: 1/60,
|
|
frame_count: 0,
|
|
draw_calls: 0,
|
|
triangles: 0,
|
|
|
|
// GPU resources
|
|
window: null,
|
|
gpu: null,
|
|
pipeline_lit: null,
|
|
pipeline_unlit: null,
|
|
pipeline_skinned: null,
|
|
sampler_nearest: null,
|
|
sampler_linear: null,
|
|
depth_texture: null,
|
|
white_texture: null,
|
|
|
|
// Camera state
|
|
camera: {
|
|
x: 0, y: 0, z: 5,
|
|
target_x: 0, target_y: 0, target_z: 0,
|
|
up_x: 0, up_y: 1, up_z: 0,
|
|
projection: "perspective",
|
|
fov: 60, near: 0.1, far: 1000,
|
|
left: -1, right: 1, bottom: -1, top: 1
|
|
},
|
|
|
|
// Lighting state
|
|
ambient: [0.2, 0.2, 0.2],
|
|
light_dir: [0.5, 1.0, 0.3],
|
|
light_color: [1, 1, 1],
|
|
light_intensity: 1.0,
|
|
|
|
// Fog state
|
|
fog_near: 10,
|
|
fog_far: 100,
|
|
fog_color: null,
|
|
|
|
// Current material
|
|
current_material: null,
|
|
|
|
// State stack
|
|
state_stack: [],
|
|
|
|
// Immediate mode
|
|
im_mode: null,
|
|
im_vertices: [],
|
|
im_color: [1, 1, 1, 1],
|
|
|
|
// Input state
|
|
keys_held: {},
|
|
keys_pressed: {},
|
|
axes: [0, 0, 0, 0],
|
|
|
|
// RNG state
|
|
rng_seed: 12345
|
|
}
|
|
|
|
// Style configurations
|
|
var _styles = {
|
|
ps1: {
|
|
id: 0,
|
|
resolution: [320, 240],
|
|
vertex_snap: true,
|
|
affine_texturing: true,
|
|
filtering: "nearest",
|
|
color_depth: 15,
|
|
dither: false
|
|
},
|
|
n64: {
|
|
id: 1,
|
|
resolution: [320, 240],
|
|
vertex_snap: false,
|
|
affine_texturing: false,
|
|
filtering: "linear",
|
|
color_depth: 16,
|
|
dither: false
|
|
},
|
|
saturn: {
|
|
id: 2,
|
|
resolution: [320, 224],
|
|
vertex_snap: false,
|
|
affine_texturing: true,
|
|
filtering: "nearest",
|
|
color_depth: 15,
|
|
dither: true
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 1) System / Style / Time / Logging
|
|
// ============================================================================
|
|
|
|
function set_style(style_name) {
|
|
if (_state.style != null) return // Already set
|
|
|
|
def style = _styles[style_name]
|
|
if (!style) {
|
|
log.console("retro3d: unknown style: " + style_name)
|
|
return
|
|
}
|
|
|
|
_state.style = style_name
|
|
_state.style_id = style.id
|
|
_state.resolution_w = style.resolution[0]
|
|
_state.resolution_h = style.resolution[1]
|
|
_state.boot_time = time_mod.number()
|
|
|
|
_init_gpu()
|
|
}
|
|
|
|
function get_style() {
|
|
return _state.style
|
|
}
|
|
|
|
function set_resolution(w, h) {
|
|
_state.resolution_w = w
|
|
_state.resolution_h = h
|
|
}
|
|
|
|
function time() {
|
|
return time_mod.number() - _state.boot_time
|
|
}
|
|
|
|
function dt() {
|
|
return _state.dt
|
|
}
|
|
|
|
function stat(name) {
|
|
if (name == "draw_calls") return _state.draw_calls
|
|
if (name == "triangles") return _state.triangles
|
|
if (name == "fps") return 1 / _state.dt
|
|
if (name == "memory_bytes") return 0
|
|
return 0
|
|
}
|
|
|
|
function log_msg() {
|
|
var args = []
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
args.push(text(arguments[i]))
|
|
}
|
|
log.console("[retro3d] " + args.join(" "))
|
|
}
|
|
|
|
// ============================================================================
|
|
// 2) Assets - Models, Textures, Audio
|
|
// ============================================================================
|
|
|
|
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 parsed = obj_loader.decode(data)
|
|
if (!parsed) return null
|
|
return _load_obj_model(parsed)
|
|
}
|
|
|
|
if (ext != "gltf" && ext != "glb") {
|
|
log.console("retro3d: unsupported model format: " + ext)
|
|
return null
|
|
}
|
|
|
|
// Parse gltf
|
|
var g = gltf.decode(data)
|
|
if (!g) return null
|
|
|
|
// Get the main buffer blob
|
|
var buffer_blob = g.buffers[0] ? g.buffers[0].blob : null
|
|
if (!buffer_blob) {
|
|
log.console("retro3d: gltf has no buffer data")
|
|
return null
|
|
}
|
|
|
|
// Build model structure
|
|
var model = {
|
|
meshes: [],
|
|
nodes: [],
|
|
root_nodes: [],
|
|
textures: [],
|
|
materials: g.materials || [],
|
|
animations: [],
|
|
animation_count: g.animations ? g.animations.length : 0,
|
|
skins: [],
|
|
_gltf: g
|
|
}
|
|
|
|
// Load textures from embedded 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)
|
|
}
|
|
}
|
|
}
|
|
model.textures.push(tex)
|
|
}
|
|
|
|
// Build node transforms (preserving hierarchy)
|
|
for (var ni = 0; ni < g.nodes.length; ni++) {
|
|
var node = g.nodes[ni]
|
|
var t = null
|
|
|
|
if (node.matrix) {
|
|
t = make_transform({
|
|
local_mat: model_c.mat4_from_array(node.matrix),
|
|
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 = make_transform({
|
|
x: trans[0], y: trans[1], z: trans[2],
|
|
qx: rot[0], qy: rot[1], qz: rot[2], qw: rot[3],
|
|
sx: scale[0], sy: scale[1], sz: scale[2]
|
|
})
|
|
}
|
|
t.mesh_index = node.mesh
|
|
t.name = node.name
|
|
t.gltf_children = node.children || []
|
|
model.nodes.push(t)
|
|
}
|
|
|
|
// Set up parent-child relationships
|
|
for (var ni = 0; ni < model.nodes.length; ni++) {
|
|
var t = model.nodes[ni]
|
|
for (var ci = 0; ci < t.gltf_children.length; ci++) {
|
|
var child_idx = t.gltf_children[ci]
|
|
if (child_idx < model.nodes.length) {
|
|
transform_set_parent(model.nodes[child_idx], t)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find root nodes (those without parents)
|
|
for (var ni = 0; ni < model.nodes.length; ni++) {
|
|
if (!model.nodes[ni].parent) {
|
|
model.root_nodes.push(model.nodes[ni])
|
|
}
|
|
}
|
|
|
|
// Process meshes
|
|
for (var mi = 0; mi < g.meshes.length; mi++) {
|
|
var mesh = g.meshes[mi]
|
|
for (var pi = 0; pi < mesh.primitives.length; pi++) {
|
|
var prim = mesh.primitives[pi]
|
|
var gpu_mesh = _process_gltf_primitive(g, buffer_blob, prim, model.textures)
|
|
if (gpu_mesh) {
|
|
gpu_mesh.name = mesh.name
|
|
gpu_mesh.mesh_index = mi
|
|
gpu_mesh.primitive_index = pi
|
|
model.meshes.push(gpu_mesh)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare animations
|
|
model.animations = anim_mod.prepare_animations(model)
|
|
model.animation_count = model.animations.length
|
|
|
|
// Prepare skins
|
|
model.skins = skin_mod.prepare_skins(model)
|
|
|
|
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
|
|
|
|
// 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 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
|
|
|
|
// Extract position data
|
|
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
|
|
)
|
|
|
|
// Extract normals
|
|
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
|
|
)
|
|
}
|
|
|
|
// Extract UVs
|
|
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
|
|
)
|
|
}
|
|
|
|
// Extract joints (for skinned meshes)
|
|
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
|
|
)
|
|
}
|
|
|
|
// Extract weights (for skinned meshes)
|
|
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
|
|
)
|
|
}
|
|
|
|
// Extract indices
|
|
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"
|
|
}
|
|
|
|
// Pack vertices
|
|
var mesh_data = {
|
|
vertex_count: vertex_count,
|
|
positions: positions,
|
|
normals: normals,
|
|
uvs: uvs,
|
|
colors: null,
|
|
joints: joints,
|
|
weights: weights
|
|
}
|
|
var packed = model_c.pack_vertices(mesh_data)
|
|
|
|
// Create GPU buffers
|
|
var vertex_buffer = _create_vertex_buffer(packed.data)
|
|
var index_buffer = indices ? _create_index_buffer(indices) : null
|
|
|
|
// Get material texture
|
|
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
|
|
}
|
|
}
|
|
|
|
function _load_obj_model(parsed) {
|
|
if (!parsed || !parsed.meshes || parsed.meshes.length == 0) return null
|
|
|
|
var model = {
|
|
meshes: [],
|
|
nodes: [],
|
|
root_nodes: [],
|
|
textures: [],
|
|
materials: [],
|
|
animations: [],
|
|
animation_count: 0
|
|
}
|
|
|
|
for (var i = 0; i < parsed.meshes.length; i++) {
|
|
var mesh = parsed.meshes[i]
|
|
var packed = model_c.pack_vertices(mesh)
|
|
var vertex_buffer = _create_vertex_buffer(packed.data)
|
|
var index_buffer = _create_index_buffer(mesh.indices)
|
|
|
|
model.meshes.push({
|
|
vertex_buffer: vertex_buffer,
|
|
index_buffer: index_buffer,
|
|
index_count: mesh.index_count,
|
|
index_type: mesh.index_type || "uint16",
|
|
vertex_count: mesh.vertex_count,
|
|
name: mesh.name,
|
|
texture: null
|
|
})
|
|
}
|
|
|
|
// Create a single root transform
|
|
var root = make_transform()
|
|
model.nodes.push(root)
|
|
model.root_nodes.push(root)
|
|
|
|
return model
|
|
}
|
|
|
|
function make_cube(w, h, d) {
|
|
var hw = w / 2, hh = h / 2, hd = d / 2
|
|
|
|
// 8 vertices, 36 indices (12 triangles)
|
|
var positions = [
|
|
// Front face
|
|
-hw, -hh, hd, hw, -hh, hd, hw, hh, hd, -hw, hh, hd,
|
|
// Back face
|
|
-hw, -hh, -hd, -hw, hh, -hd, hw, hh, -hd, hw, -hh, -hd,
|
|
// Top face
|
|
-hw, hh, -hd, -hw, hh, hd, hw, hh, hd, hw, hh, -hd,
|
|
// Bottom face
|
|
-hw, -hh, -hd, hw, -hh, -hd, hw, -hh, hd, -hw, -hh, hd,
|
|
// Right face
|
|
hw, -hh, -hd, hw, hh, -hd, hw, hh, hd, hw, -hh, hd,
|
|
// Left face
|
|
-hw, -hh, -hd, -hw, -hh, hd, -hw, hh, hd, -hw, hh, -hd
|
|
]
|
|
|
|
var normals = [
|
|
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
|
|
0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1,
|
|
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
|
|
0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0,
|
|
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
|
|
-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0
|
|
]
|
|
|
|
var uvs = [
|
|
0,1, 1,1, 1,0, 0,0,
|
|
1,1, 1,0, 0,0, 0,1,
|
|
0,0, 0,1, 1,1, 1,0,
|
|
0,1, 1,1, 1,0, 0,0,
|
|
1,1, 1,0, 0,0, 0,1,
|
|
0,1, 1,1, 1,0, 0,0
|
|
]
|
|
|
|
var indices = [
|
|
0, 1, 2, 0, 2, 3,
|
|
4, 5, 6, 4, 6, 7,
|
|
8, 9,10, 8,10,11,
|
|
12,13,14, 12,14,15,
|
|
16,17,18, 16,18,19,
|
|
20,21,22, 20,22,23
|
|
]
|
|
|
|
return _make_model_from_arrays(positions, normals, uvs, indices)
|
|
}
|
|
|
|
function make_sphere(r, segments) {
|
|
if (!segments) segments = 12
|
|
var positions = []
|
|
var normals = []
|
|
var uvs = []
|
|
var indices = []
|
|
|
|
for (var y = 0; y <= segments; y++) {
|
|
var v = y / segments
|
|
var theta = v * 3.14159265
|
|
|
|
for (var x = 0; x <= segments; x++) {
|
|
var u = x / segments
|
|
var phi = u * 2 * 3.14159265
|
|
|
|
var nx = Math.sin(theta) * Math.cos(phi)
|
|
var ny = Math.cos(theta)
|
|
var nz = Math.sin(theta) * Math.sin(phi)
|
|
|
|
positions.push(nx * r, ny * r, nz * r)
|
|
normals.push(nx, ny, nz)
|
|
uvs.push(u, v)
|
|
}
|
|
}
|
|
|
|
for (var y = 0; y < segments; y++) {
|
|
for (var x = 0; x < segments; x++) {
|
|
var i = y * (segments + 1) + x
|
|
indices.push(i, i + 1, i + segments + 1)
|
|
indices.push(i + 1, i + segments + 2, i + segments + 1)
|
|
}
|
|
}
|
|
|
|
return _make_model_from_arrays(positions, normals, uvs, indices)
|
|
}
|
|
|
|
function make_cylinder(r, h, segments) {
|
|
if (!segments) segments = 12
|
|
var positions = []
|
|
var normals = []
|
|
var uvs = []
|
|
var indices = []
|
|
var hh = h / 2
|
|
|
|
// Side vertices
|
|
for (var i = 0; i <= segments; i++) {
|
|
var u = i / segments
|
|
var angle = u * 2 * 3.14159265
|
|
var nx = Math.cos(angle)
|
|
var nz = Math.sin(angle)
|
|
|
|
positions.push(nx * r, -hh, nz * r)
|
|
normals.push(nx, 0, nz)
|
|
uvs.push(u, 1)
|
|
|
|
positions.push(nx * r, hh, nz * r)
|
|
normals.push(nx, 0, nz)
|
|
uvs.push(u, 0)
|
|
}
|
|
|
|
// Side indices
|
|
for (var i = 0; i < segments; i++) {
|
|
var base = i * 2
|
|
indices.push(base, base + 1, base + 2)
|
|
indices.push(base + 1, base + 3, base + 2)
|
|
}
|
|
|
|
return _make_model_from_arrays(positions, normals, uvs, indices)
|
|
}
|
|
|
|
function make_plane(w, h) {
|
|
var hw = w / 2, hh = h / 2
|
|
var positions = [-hw, 0, -hh, hw, 0, -hh, hw, 0, hh, -hw, 0, hh]
|
|
var normals = [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]
|
|
var uvs = [0, 0, 1, 0, 1, 1, 0, 1]
|
|
var indices = [0, 1, 2, 0, 2, 3]
|
|
return _make_model_from_arrays(positions, normals, uvs, indices)
|
|
}
|
|
|
|
function make_model(vertices, indices, uvs_in, colors_in) {
|
|
var vertex_count = vertices.length / 3
|
|
var positions = vertices
|
|
var normals = []
|
|
var uvs = uvs_in || []
|
|
var colors = colors_in || []
|
|
|
|
// Generate flat normals
|
|
for (var i = 0; i < vertex_count; i++) {
|
|
normals.push(0, 1, 0)
|
|
}
|
|
|
|
return _make_model_from_arrays(positions, normals, uvs, indices, colors)
|
|
}
|
|
|
|
function load_texture(path) {
|
|
var png = use('cell-image/png')
|
|
var data = io.slurp(path)
|
|
if (!data) return null
|
|
|
|
var img = png.decode(data)
|
|
if (!img) return null
|
|
|
|
return _create_texture(img.width, img.height, img.pixels)
|
|
}
|
|
|
|
function make_texture(w, h, rgba_u8) {
|
|
return _create_texture(w, h, rgba_u8)
|
|
}
|
|
|
|
// Audio stubs (to be implemented with audio backend)
|
|
function load_sound(path) { return null }
|
|
function play_sound(sound, opts) { return null }
|
|
function stop_sound(voice_id) {}
|
|
function load_music(path) { return null }
|
|
function play_music(music, loop) {}
|
|
function stop_music() {}
|
|
function set_music_volume(v) {}
|
|
function set_sfx_volume(v) {}
|
|
|
|
// ============================================================================
|
|
// 3) Transforms - Hierarchical with dirty flags
|
|
// ============================================================================
|
|
|
|
function make_transform(opts) {
|
|
opts = opts || {}
|
|
return {
|
|
parent: opts.parent || null,
|
|
children: [],
|
|
|
|
// TRS (authoring) - translation
|
|
x: opts.x || 0,
|
|
y: opts.y || 0,
|
|
z: opts.z || 0,
|
|
// Quaternion rotation
|
|
qx: opts.qx || 0,
|
|
qy: opts.qy || 0,
|
|
qz: opts.qz || 0,
|
|
qw: opts.qw != null ? opts.qw : 1,
|
|
// Scale
|
|
sx: opts.sx != null ? opts.sx : 1,
|
|
sy: opts.sy != null ? opts.sy : 1,
|
|
sz: opts.sz != null ? opts.sz : 1,
|
|
|
|
// Matrices (opaque blobs)
|
|
local_mat: opts.local_mat || null,
|
|
world_mat: null,
|
|
|
|
// Authority: if true, local_mat is used directly, TRS ignored
|
|
has_local_mat: opts.has_local_mat || false,
|
|
|
|
// Cache invalidation
|
|
dirty_local: true,
|
|
dirty_world: true
|
|
}
|
|
}
|
|
|
|
function transform_set_parent(child, parent) {
|
|
if (child.parent) {
|
|
var idx = child.parent.children.indexOf(child)
|
|
if (idx >= 0) child.parent.children.splice(idx, 1)
|
|
}
|
|
child.parent = parent
|
|
if (parent) parent.children.push(child)
|
|
transform_mark_dirty(child)
|
|
}
|
|
|
|
function transform_mark_dirty(t) {
|
|
t.dirty_local = true
|
|
t.dirty_world = true
|
|
for (var i = 0; i < t.children.length; i++) {
|
|
transform_mark_world_dirty(t.children[i])
|
|
}
|
|
}
|
|
|
|
function transform_mark_world_dirty(t) {
|
|
t.dirty_world = true
|
|
for (var i = 0; i < t.children.length; i++) {
|
|
transform_mark_world_dirty(t.children[i])
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Build from TRS
|
|
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 transform_set_position(t, x, y, z) {
|
|
t.x = x; t.y = y; t.z = z
|
|
t.has_local_mat = false
|
|
transform_mark_dirty(t)
|
|
}
|
|
|
|
function transform_set_rotation_quat(t, qx, qy, qz, qw) {
|
|
t.qx = qx; t.qy = qy; t.qz = qz; t.qw = qw
|
|
t.has_local_mat = false
|
|
transform_mark_dirty(t)
|
|
}
|
|
|
|
function transform_set_scale(t, sx, sy, sz) {
|
|
t.sx = sx; t.sy = sy; t.sz = sz
|
|
t.has_local_mat = false
|
|
transform_mark_dirty(t)
|
|
}
|
|
|
|
function transform_set_matrix(t, mat) {
|
|
t.local_mat = mat
|
|
t.has_local_mat = true
|
|
transform_mark_dirty(t)
|
|
}
|
|
|
|
// ============================================================================
|
|
// 4) Camera & Projection
|
|
// ============================================================================
|
|
|
|
function camera_look_at(ex, ey, ez, tx, ty, tz, upx, upy, upz) {
|
|
_state.camera.x = ex
|
|
_state.camera.y = ey
|
|
_state.camera.z = ez
|
|
_state.camera.target_x = tx
|
|
_state.camera.target_y = ty
|
|
_state.camera.target_z = tz
|
|
_state.camera.up_x = upx != null ? upx : 0
|
|
_state.camera.up_y = upy != null ? upy : 1
|
|
_state.camera.up_z = upz != null ? upz : 0
|
|
}
|
|
|
|
function camera_perspective(fov_deg, near, far) {
|
|
_state.camera.projection = "perspective"
|
|
_state.camera.fov = fov_deg || 60
|
|
_state.camera.near = near || 0.1
|
|
_state.camera.far = far || 1000
|
|
}
|
|
|
|
function camera_ortho(left, right, bottom, top, near, far) {
|
|
_state.camera.projection = "ortho"
|
|
_state.camera.left = left
|
|
_state.camera.right = right
|
|
_state.camera.bottom = bottom
|
|
_state.camera.top = top
|
|
_state.camera.near = near || -1
|
|
_state.camera.far = far || 1
|
|
}
|
|
|
|
function camera_get() {
|
|
return Object.assign({}, _state.camera)
|
|
}
|
|
|
|
// ============================================================================
|
|
// 5) Render State Stack
|
|
// ============================================================================
|
|
|
|
function push_state() {
|
|
_state.state_stack.push({
|
|
camera: Object.assign({}, _state.camera),
|
|
ambient: _state.ambient.slice(),
|
|
light_dir: _state.light_dir.slice(),
|
|
light_color: _state.light_color.slice(),
|
|
light_intensity: _state.light_intensity,
|
|
fog_near: _state.fog_near,
|
|
fog_far: _state.fog_far,
|
|
fog_color: _state.fog_color ? _state.fog_color.slice() : null,
|
|
current_material: _state.current_material,
|
|
im_color: _state.im_color.slice()
|
|
})
|
|
}
|
|
|
|
function pop_state() {
|
|
if (_state.state_stack.length == 0) return
|
|
var s = _state.state_stack.pop()
|
|
_state.camera = s.camera
|
|
_state.ambient = s.ambient
|
|
_state.light_dir = s.light_dir
|
|
_state.light_color = s.light_color
|
|
_state.light_intensity = s.light_intensity
|
|
_state.fog_near = s.fog_near
|
|
_state.fog_far = s.fog_far
|
|
_state.fog_color = s.fog_color
|
|
_state.current_material = s.current_material
|
|
_state.im_color = s.im_color
|
|
}
|
|
|
|
// ============================================================================
|
|
// 6) Materials, Lighting, Fog
|
|
// ============================================================================
|
|
|
|
function make_material(kind, opts) {
|
|
opts = opts || {}
|
|
return {
|
|
kind: kind,
|
|
texture: opts.texture || null,
|
|
color: opts.color || [1, 1, 1, 1]
|
|
}
|
|
}
|
|
|
|
function set_material(material) {
|
|
_state.current_material = material
|
|
}
|
|
|
|
function set_ambient(r, g, b) {
|
|
_state.ambient = [r, g, b]
|
|
}
|
|
|
|
function set_light_dir(dx, dy, dz, r, g, b, intensity) {
|
|
var len = Math.sqrt(dx*dx + dy*dy + dz*dz)
|
|
_state.light_dir = [dx/len, dy/len, dz/len]
|
|
_state.light_color = [r, g, b]
|
|
_state.light_intensity = intensity != null ? intensity : 1.0
|
|
}
|
|
|
|
function set_fog(near, far, color_or_null) {
|
|
_state.fog_near = near
|
|
_state.fog_far = far
|
|
_state.fog_color = color_or_null
|
|
}
|
|
|
|
// ============================================================================
|
|
// 7) Drawing - Models & Immediate Mode
|
|
// ============================================================================
|
|
|
|
function clear(r, g, b, a, clear_depth) {
|
|
if (a == null) a = 1.0
|
|
if (clear_depth == null) clear_depth = true
|
|
_state._clear_color = [r, g, b, a]
|
|
_state._clear_depth = clear_depth
|
|
}
|
|
|
|
function clear_depth() {
|
|
_state._clear_depth_only = true
|
|
}
|
|
|
|
function draw_model(model, transform, anim_instance) {
|
|
if (!model || !model.meshes) return
|
|
|
|
// Get view and projection matrices
|
|
var view_matrix = _compute_view_matrix()
|
|
var proj_matrix = _compute_projection_matrix()
|
|
|
|
// If transform is provided, use it as an additional parent for the model's root nodes
|
|
var extra_transform = transform ? transform_get_world_matrix(transform) : null
|
|
|
|
// Build joint palettes for all skins (if any)
|
|
var skin_palettes = []
|
|
if (model.skins && model.skins.length > 0) {
|
|
for (var si = 0; si < model.skins.length; si++) {
|
|
var skin = model.skins[si]
|
|
// Collect world matrices for each joint
|
|
var world_matrices = []
|
|
for (var j = 0; j < skin.joints.length; j++) {
|
|
var node_idx = skin.joints[j]
|
|
var jnode = model.nodes[node_idx]
|
|
if (jnode) {
|
|
var jworld = transform_get_world_matrix(jnode)
|
|
// Apply extra transform if provided
|
|
if (extra_transform) {
|
|
jworld = model_c.mat4_mul(extra_transform, jworld)
|
|
}
|
|
world_matrices.push(jworld)
|
|
} else {
|
|
world_matrices.push(model_c.mat4_identity())
|
|
}
|
|
}
|
|
// Build palette using C function
|
|
var palette = model_c.build_joint_palette(world_matrices, skin.inv_bind, skin.joint_count)
|
|
skin_palettes.push(palette)
|
|
}
|
|
}
|
|
|
|
// Draw each node that has a mesh
|
|
for (var ni = 0; ni < model.nodes.length; ni++) {
|
|
var node = model.nodes[ni]
|
|
if (node.mesh_index == null) continue
|
|
|
|
// Find meshes for this node
|
|
for (var mi = 0; mi < model.meshes.length; mi++) {
|
|
var mesh = model.meshes[mi]
|
|
if (mesh.mesh_index != node.mesh_index) continue
|
|
|
|
// Get world matrix for this node
|
|
var node_world = transform_get_world_matrix(node)
|
|
|
|
// Apply extra transform if provided
|
|
var world_matrix = extra_transform
|
|
? model_c.mat4_mul(extra_transform, node_world)
|
|
: node_world
|
|
|
|
// Get material/texture
|
|
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)
|
|
|
|
// Build uniforms in cell script
|
|
var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, tint)
|
|
|
|
// Get palette for skinned mesh (use first skin for now)
|
|
var palette = null
|
|
if (mesh.skinned && skin_palettes.length > 0) {
|
|
palette = skin_palettes[0]
|
|
}
|
|
|
|
_draw_mesh(mesh, uniforms, tex, mat ? mat.kind : "lit", palette)
|
|
_state.draw_calls++
|
|
_state.triangles += mesh.index_count / 3
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
// Get palette for skinned mesh
|
|
var palette = null
|
|
if (mesh.skinned && skin_palettes.length > 0) {
|
|
palette = skin_palettes[0]
|
|
}
|
|
|
|
_draw_mesh(mesh, uniforms, tex, mat ? mat.kind : "lit", palette)
|
|
_state.draw_calls++
|
|
_state.triangles += mesh.index_count / 3
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build uniform buffer using C helper (blob API doesn't have write_f32)
|
|
function _build_uniforms(model_mat, view_mat, proj_mat, tint) {
|
|
return model_c.build_uniforms({
|
|
model: model_mat,
|
|
view: view_mat,
|
|
projection: proj_mat,
|
|
ambient: _state.ambient,
|
|
light_dir: _state.light_dir,
|
|
light_color: _state.light_color,
|
|
light_intensity: _state.light_intensity,
|
|
fog_near: _state.fog_near,
|
|
fog_far: _state.fog_far,
|
|
fog_color: _state.fog_color,
|
|
tint: tint,
|
|
style_id: _state.style_id,
|
|
resolution_w: _state.resolution_w,
|
|
resolution_h: _state.resolution_h
|
|
})
|
|
}
|
|
|
|
// Immediate mode
|
|
function begin_triangles() { _state.im_mode = "triangles"; _state.im_vertices = [] }
|
|
function begin_lines() { _state.im_mode = "lines"; _state.im_vertices = [] }
|
|
function begin_points() { _state.im_mode = "points"; _state.im_vertices = [] }
|
|
|
|
function color(r, g, b, a) {
|
|
_state.im_color = [r, g, b, a != null ? a : 1.0]
|
|
}
|
|
|
|
function vertex(x, y, z, u, v) {
|
|
_state.im_vertices.push({
|
|
x: x, y: y, z: z,
|
|
u: u || 0, v: v || 0,
|
|
color: _state.im_color.slice()
|
|
})
|
|
}
|
|
|
|
function end() {
|
|
if (_state.im_vertices.length == 0) return
|
|
// Flush immediate mode geometry
|
|
_flush_immediate()
|
|
_state.im_mode = null
|
|
_state.im_vertices = []
|
|
}
|
|
|
|
// ============================================================================
|
|
// 8) Animation
|
|
// ============================================================================
|
|
|
|
function anim_instance(model) {
|
|
return anim_mod.create_instance(model)
|
|
}
|
|
|
|
function anim_clip_count(model_or_instance) {
|
|
if (model_or_instance.animations) {
|
|
// It's an instance
|
|
return anim_mod.clip_count(model_or_instance)
|
|
}
|
|
// It's a model
|
|
return model_or_instance.animation_count || 0
|
|
}
|
|
|
|
function anim_clip_duration(model_or_instance, clip_index) {
|
|
if (model_or_instance.animations && model_or_instance.model) {
|
|
// It's an instance
|
|
return anim_mod.clip_duration(model_or_instance, clip_index)
|
|
}
|
|
// It's a model - create temp instance
|
|
if (!model_or_instance.animations || clip_index >= model_or_instance.animations.length) return 0
|
|
return model_or_instance.animations[clip_index].duration || 0
|
|
}
|
|
|
|
function anim_clip_name(instance, clip_index) {
|
|
return anim_mod.clip_name(instance, clip_index)
|
|
}
|
|
|
|
function anim_find_clip(instance, name) {
|
|
return anim_mod.find_clip(instance, name)
|
|
}
|
|
|
|
function anim_play(anim, clip_index, loop) {
|
|
anim_mod.play(anim, clip_index, loop)
|
|
}
|
|
|
|
function anim_stop(anim) {
|
|
anim_mod.stop(anim)
|
|
}
|
|
|
|
function anim_set_time(anim, seconds) {
|
|
anim_mod.set_time(anim, seconds)
|
|
}
|
|
|
|
function anim_set_speed(anim, speed) {
|
|
anim_mod.set_speed(anim, speed)
|
|
}
|
|
|
|
function anim_update(anim, dt_val) {
|
|
dt_val = dt_val != null ? dt_val : _state.dt
|
|
anim_mod.update(anim, dt_val)
|
|
}
|
|
|
|
function anim_apply(anim) {
|
|
anim_mod.apply(anim)
|
|
}
|
|
|
|
function anim_pop_events(anim) {
|
|
return []
|
|
}
|
|
|
|
// ============================================================================
|
|
// 8b) Skinning
|
|
// ============================================================================
|
|
|
|
function skin_get(model, skin_index) {
|
|
if (!model.skins || skin_index >= model.skins.length) return null
|
|
return model.skins[skin_index]
|
|
}
|
|
|
|
function skin_build_palette(skin, model) {
|
|
return skin_mod.build_palette(skin, model, { transform_get_world_matrix: transform_get_world_matrix })
|
|
}
|
|
|
|
function skin_get_joint_world(skin, joint_index, model) {
|
|
return skin_mod.get_joint_world(skin, joint_index, model, { transform_get_world_matrix: transform_get_world_matrix })
|
|
}
|
|
|
|
function skin_find_joint(skin, name, model) {
|
|
return skin_mod.find_joint(skin, name, model)
|
|
}
|
|
|
|
function skin_attachment_transform(skin, joint_index, model, offset_mat) {
|
|
return skin_mod.attachment_transform(skin, joint_index, model, { transform_get_world_matrix: transform_get_world_matrix }, offset_mat)
|
|
}
|
|
|
|
// ============================================================================
|
|
// 9) Collision (stubs for v1)
|
|
// ============================================================================
|
|
|
|
var _colliders = []
|
|
var _collider_id = 0
|
|
|
|
function add_collider_sphere(transform, radius, layer_mask, user) {
|
|
var c = {
|
|
id: _collider_id++,
|
|
type: "sphere",
|
|
transform: transform,
|
|
radius: radius,
|
|
layer_mask: layer_mask || 1,
|
|
user: user
|
|
}
|
|
_colliders.push(c)
|
|
return c
|
|
}
|
|
|
|
function add_collider_box(transform, sx, sy, sz, layer_mask, user) {
|
|
var c = {
|
|
id: _collider_id++,
|
|
type: "box",
|
|
transform: transform,
|
|
sx: sx, sy: sy, sz: sz,
|
|
layer_mask: layer_mask || 1,
|
|
user: user
|
|
}
|
|
_colliders.push(c)
|
|
return c
|
|
}
|
|
|
|
function remove_collider(collider) {
|
|
for (var i = 0; i < _colliders.length; i++) {
|
|
if (_colliders[i].id == collider.id) {
|
|
_colliders.splice(i, 1)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
function overlaps(layer_mask_a, layer_mask_b) {
|
|
// Simple O(n^2) collision check
|
|
var results = []
|
|
for (var i = 0; i < _colliders.length; i++) {
|
|
for (var j = i + 1; j < _colliders.length; j++) {
|
|
var a = _colliders[i]
|
|
var b = _colliders[j]
|
|
|
|
if (layer_mask_a != null && !(a.layer_mask & layer_mask_a)) continue
|
|
if (layer_mask_b != null && !(b.layer_mask & layer_mask_b)) continue
|
|
|
|
if (_check_collision(a, b)) {
|
|
results.push({a: a, b: b})
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
function raycast(ox, oy, oz, dx, dy, dz, max_dist, layer_mask) {
|
|
// Stub - returns null for now
|
|
return null
|
|
}
|
|
|
|
function _check_collision(a, b) {
|
|
// Simple sphere-sphere for now
|
|
var ax = a.transform.x, ay = a.transform.y, az = a.transform.z
|
|
var bx = b.transform.x, by = b.transform.y, bz = b.transform.z
|
|
var dx = bx - ax, dy = by - ay, dz = bz - az
|
|
var dist = Math.sqrt(dx*dx + dy*dy + dz*dz)
|
|
var ra = a.radius || Math.max(a.sx || 0, a.sy || 0, a.sz || 0)
|
|
var rb = b.radius || Math.max(b.sx || 0, b.sy || 0, b.sz || 0)
|
|
return dist < ra + rb
|
|
}
|
|
|
|
// ============================================================================
|
|
// 10) Input
|
|
// ============================================================================
|
|
|
|
function btn(id, player) {
|
|
// Map button IDs to keyboard keys for now
|
|
var key_map = ['z', 'x', 'c', 'v', 'a', 's', 'return', 'escape']
|
|
var key = key_map[id]
|
|
return _state.keys_held[key] || false
|
|
}
|
|
|
|
function btnp(id, player) {
|
|
var key_map = ['z', 'x', 'c', 'v', 'a', 's', 'return', 'escape']
|
|
var key = key_map[id]
|
|
return _state.keys_pressed[key] || false
|
|
}
|
|
|
|
function axis(id, player) {
|
|
return _state.axes[id] || 0
|
|
}
|
|
|
|
// ============================================================================
|
|
// 11) RNG & Math Helpers
|
|
// ============================================================================
|
|
|
|
function seed(seed_int) {
|
|
_state.rng_seed = seed_int
|
|
}
|
|
|
|
function rand() {
|
|
// Simple LCG
|
|
_state.rng_seed = (_state.rng_seed * 1103515245 + 12345) & 0x7fffffff
|
|
return _state.rng_seed / 0x7fffffff
|
|
}
|
|
|
|
function irand(min_inclusive, max_inclusive) {
|
|
return Math.floor(rand() * (max_inclusive - min_inclusive + 1)) + min_inclusive
|
|
}
|
|
|
|
// ============================================================================
|
|
// Internal GPU functions
|
|
// ============================================================================
|
|
|
|
function _init_gpu() {
|
|
// Create window
|
|
_state.window = new video.window({
|
|
title: "retro3d",
|
|
width: _state.resolution_w * 2,
|
|
height: _state.resolution_h * 2
|
|
})
|
|
|
|
// Create GPU device
|
|
_state.gpu = new gpu_mod.gpu({ debug: true, shaders_msl: true })
|
|
_state.gpu.claim_window(_state.window)
|
|
|
|
// Load shaders
|
|
var vert_code = io.slurp("shaders/retro3d.vert.msl")
|
|
var frag_code = io.slurp("shaders/retro3d.frag.msl")
|
|
|
|
if (!vert_code || !frag_code) {
|
|
log.console("retro3d: failed to load shaders")
|
|
return
|
|
}
|
|
|
|
var vert_shader = new gpu_mod.shader(_state.gpu, {
|
|
code: vert_code,
|
|
stage: "vertex",
|
|
format: "msl",
|
|
entrypoint: "vertex_main",
|
|
num_uniform_buffers: 2
|
|
})
|
|
|
|
var frag_shader = new gpu_mod.shader(_state.gpu, {
|
|
code: frag_code,
|
|
stage: "fragment",
|
|
format: "msl",
|
|
entrypoint: "fragment_main",
|
|
num_uniform_buffers: 2,
|
|
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"
|
|
}
|
|
})
|
|
|
|
// Create skinned pipeline (for animated meshes with joints/weights)
|
|
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, {
|
|
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 samplers
|
|
var style = _styles[_state.style]
|
|
_state.sampler_nearest = new gpu_mod.sampler(_state.gpu, {
|
|
min_filter: "nearest",
|
|
mag_filter: "nearest",
|
|
u: "repeat",
|
|
v: "repeat"
|
|
})
|
|
|
|
_state.sampler_linear = new gpu_mod.sampler(_state.gpu, {
|
|
min_filter: "linear",
|
|
mag_filter: "linear",
|
|
u: "repeat",
|
|
v: "repeat"
|
|
})
|
|
|
|
// Create depth texture
|
|
_state.depth_texture = new gpu_mod.texture(_state.gpu, {
|
|
width: _state.resolution_w * 2,
|
|
height: _state.resolution_h * 2,
|
|
format: "d32 float s8",
|
|
type: "2d",
|
|
layers: 1,
|
|
mip_levels: 1,
|
|
depth_target: true
|
|
})
|
|
|
|
// Create white texture (1x1)
|
|
var white_pixels = new blob_mod(32, true)
|
|
|
|
_state.white_texture = _create_texture(1, 1, stone(white_pixels))
|
|
}
|
|
|
|
function _create_vertex_buffer(data) {
|
|
var size = data.length / 8
|
|
var buffer = new gpu_mod.buffer(_state.gpu, {
|
|
size: size,
|
|
vertex: true
|
|
})
|
|
|
|
var transfer = new gpu_mod.transfer_buffer(_state.gpu, {
|
|
size: size,
|
|
usage: "upload"
|
|
})
|
|
|
|
transfer.copy_blob(_state.gpu, data)
|
|
|
|
var cmd = _state.gpu.acquire_cmd_buffer()
|
|
var copy = cmd.copy_pass()
|
|
copy.upload_to_buffer(
|
|
{ transfer_buffer: transfer, offset: 0 },
|
|
{ buffer: buffer, offset: 0, size: size },
|
|
false
|
|
)
|
|
copy.end()
|
|
cmd.submit()
|
|
|
|
return buffer
|
|
}
|
|
|
|
function _create_index_buffer(data) {
|
|
var size = data.length / 8
|
|
var buffer = new gpu_mod.buffer(_state.gpu, {
|
|
size: size,
|
|
index: true
|
|
})
|
|
|
|
var transfer = new gpu_mod.transfer_buffer(_state.gpu, {
|
|
size: size,
|
|
usage: "upload"
|
|
})
|
|
|
|
transfer.copy_blob(_state.gpu, data)
|
|
|
|
var cmd = _state.gpu.acquire_cmd_buffer()
|
|
var copy = cmd.copy_pass()
|
|
copy.upload_to_buffer(
|
|
{ transfer_buffer: transfer, offset: 0 },
|
|
{ buffer: buffer, offset: 0, size: size },
|
|
false
|
|
)
|
|
copy.end()
|
|
cmd.submit()
|
|
|
|
return buffer
|
|
}
|
|
|
|
function _create_texture(w, h, pixels) {
|
|
var tex = new gpu_mod.texture(_state.gpu, {
|
|
width: w,
|
|
height: h,
|
|
format: "rgba8",
|
|
type: "2d",
|
|
layers: 1,
|
|
mip_levels: 1,
|
|
sampler: true
|
|
})
|
|
|
|
var size = w * h * 4
|
|
var transfer = new gpu_mod.transfer_buffer(_state.gpu, {
|
|
size: size,
|
|
usage: "upload"
|
|
})
|
|
|
|
transfer.copy_blob(_state.gpu, pixels)
|
|
|
|
var cmd = _state.gpu.acquire_cmd_buffer()
|
|
var copy = cmd.copy_pass()
|
|
copy.upload_to_texture(
|
|
{ transfer_buffer: transfer, offset: 0, pixels_per_row: w, rows_per_layer: h },
|
|
{ texture: tex, x: 0, y: 0, z: 0, w: w, h: h, d: 1 },
|
|
false
|
|
)
|
|
copy.end()
|
|
cmd.submit()
|
|
|
|
tex.width = w
|
|
tex.height = h
|
|
return tex
|
|
}
|
|
|
|
function _make_model_from_arrays(positions, normals, uvs, indices, colors) {
|
|
var vertex_count = positions.length / 3
|
|
|
|
// Build mesh object
|
|
var mesh = {
|
|
vertex_count: vertex_count,
|
|
index_count: indices.length
|
|
}
|
|
|
|
mesh.positions = model_c.f32_blob(positions)
|
|
mesh.normals = normals && normals.length > 0 ? model_c.f32_blob(normals) : null
|
|
mesh.uvs = uvs && uvs.length > 0 ? model_c.f32_blob(uvs) : null
|
|
mesh.colors = colors && colors.length > 0 ? model_c.f32_blob(colors) : null
|
|
|
|
mesh.indices = model_c.u16_blob(indices)
|
|
mesh.index_type = "uint16"
|
|
|
|
// Pack and create GPU buffers
|
|
var packed = model_c.pack_vertices(mesh)
|
|
|
|
return {
|
|
meshes: [{
|
|
vertex_buffer: _create_vertex_buffer(packed.data),
|
|
index_buffer: _create_index_buffer(mesh.indices),
|
|
index_count: mesh.index_count,
|
|
index_type: "uint16",
|
|
vertex_count: vertex_count,
|
|
texture: null,
|
|
mesh_index: 0
|
|
}],
|
|
nodes: [],
|
|
root_nodes: [],
|
|
textures: [],
|
|
materials: [],
|
|
animations: [],
|
|
animation_count: 0
|
|
}
|
|
}
|
|
|
|
function _compute_view_matrix() {
|
|
var c = _state.camera
|
|
return model_c.compute_view_matrix(
|
|
c.x, c.y, c.z,
|
|
c.target_x, c.target_y, c.target_z,
|
|
c.up_x, c.up_y, c.up_z
|
|
)
|
|
}
|
|
|
|
function _compute_projection_matrix() {
|
|
var c = _state.camera
|
|
var aspect = _state.resolution_w / _state.resolution_h
|
|
|
|
if (c.projection == "perspective") {
|
|
return model_c.compute_perspective(c.fov, aspect, c.near, c.far)
|
|
} else {
|
|
return model_c.compute_ortho(c.left, c.right, c.bottom, c.top, c.near, c.far)
|
|
}
|
|
}
|
|
|
|
function _draw_mesh(mesh, uniforms, texture, kind, palette) {
|
|
// This will be called during render pass
|
|
_state._pending_draws = _state._pending_draws || []
|
|
_state._pending_draws.push({
|
|
mesh: mesh,
|
|
uniforms: uniforms,
|
|
texture: texture,
|
|
kind: kind,
|
|
palette: palette
|
|
})
|
|
}
|
|
|
|
function _flush_immediate() {
|
|
// Convert immediate mode vertices to a temporary mesh and draw
|
|
if (_state.im_vertices.length == 0) return
|
|
|
|
var verts = _state.im_vertices
|
|
var positions = []
|
|
var normals = []
|
|
var uvs = []
|
|
var colors = []
|
|
var indices = []
|
|
|
|
for (var i = 0; i < verts.length; i++) {
|
|
var v = verts[i]
|
|
positions.push(v.x, v.y, v.z)
|
|
normals.push(0, 1, 0)
|
|
uvs.push(v.u, v.v)
|
|
colors.push(v.color[0], v.color[1], v.color[2], v.color[3])
|
|
indices.push(i)
|
|
}
|
|
|
|
var model = _make_model_from_arrays(positions, normals, uvs, indices, colors)
|
|
var transform = make_transform()
|
|
draw_model(model, transform)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Frame rendering (called by cart runner)
|
|
// ============================================================================
|
|
|
|
function _begin_frame() {
|
|
_state.draw_calls = 0
|
|
_state.triangles = 0
|
|
_state.keys_pressed = {}
|
|
_state._pending_draws = []
|
|
_state._clear_color = [0, 0, 0, 1]
|
|
_state._clear_depth = true
|
|
}
|
|
|
|
function _process_events() {
|
|
var ev
|
|
while ((ev = events.poll()) != null) {
|
|
if (ev.type == "quit" || ev.type == "window_close_requested") {
|
|
return false
|
|
}
|
|
if (ev.type == "key_down" || ev.type == "key_up") {
|
|
var key_name = keyboard.get_key_name(ev.key).toLowerCase()
|
|
var pressed = ev.type == "key_down"
|
|
|
|
if (pressed && !_state.keys_held[key_name]) {
|
|
_state.keys_pressed[key_name] = true
|
|
}
|
|
_state.keys_held[key_name] = pressed
|
|
|
|
// Map WASD to axes
|
|
if (key_name == "w") _state.axes[1] = pressed ? -1 : 0
|
|
if (key_name == "s") _state.axes[1] = pressed ? 1 : 0
|
|
if (key_name == "a") _state.axes[0] = pressed ? -1 : 0
|
|
if (key_name == "d") _state.axes[0] = pressed ? 1 : 0
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
function _end_frame() {
|
|
if (!_state.gpu) return
|
|
|
|
var cmd = _state.gpu.acquire_cmd_buffer()
|
|
|
|
// Get swapchain pass instead
|
|
var pass_desc = {
|
|
color_targets: [{
|
|
texture: null, // Will use swapchain
|
|
load: "clear",
|
|
store: "store",
|
|
clear_color: {
|
|
r: _state._clear_color[0],
|
|
g: _state._clear_color[1],
|
|
b: _state._clear_color[2],
|
|
a: _state._clear_color[3]
|
|
}
|
|
}]
|
|
}
|
|
|
|
if (_state.depth_texture) {
|
|
pass_desc.depth_stencil = {
|
|
texture: _state.depth_texture,
|
|
load: _state._clear_depth ? "clear" : "load",
|
|
store: "dont_care",
|
|
stencil_load: "clear",
|
|
stencil_store: "dont_care",
|
|
clear: 1.0,
|
|
clear_stencil: 0
|
|
}
|
|
}
|
|
|
|
var swap_pass = cmd.swapchain_pass(_state.window, pass_desc)
|
|
|
|
// Draw all pending meshes
|
|
var draws = _state._pending_draws || []
|
|
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)
|
|
|
|
// Shaders use [[buffer(1)]] for uniforms, [[buffer(2)]] for joint palette
|
|
cmd.push_vertex_uniform_data(1, d.uniforms)
|
|
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
|
|
swap_pass.bind_fragment_samplers(0, [{ texture: d.texture, sampler: sampler }])
|
|
|
|
swap_pass.draw_indexed(d.mesh.index_count, 1, 0, 0, 0)
|
|
}
|
|
|
|
swap_pass.end()
|
|
cmd.submit()
|
|
|
|
_state.frame_count++
|
|
}
|
|
|
|
// Export the module
|
|
return {
|
|
set_style: set_style,
|
|
get_style: get_style,
|
|
set_resolution: set_resolution,
|
|
time: time,
|
|
dt: dt,
|
|
stat: stat,
|
|
log: log_msg,
|
|
|
|
load_model: load_model,
|
|
make_cube: make_cube,
|
|
make_sphere: make_sphere,
|
|
make_cylinder: make_cylinder,
|
|
make_plane: make_plane,
|
|
make_model: make_model,
|
|
load_texture: load_texture,
|
|
make_texture: make_texture,
|
|
load_sound: load_sound,
|
|
play_sound: play_sound,
|
|
stop_sound: stop_sound,
|
|
load_music: load_music,
|
|
play_music: play_music,
|
|
stop_music: stop_music,
|
|
set_music_volume: set_music_volume,
|
|
set_sfx_volume: set_sfx_volume,
|
|
|
|
make_transform: make_transform,
|
|
transform_set_parent: transform_set_parent,
|
|
transform_set_position: transform_set_position,
|
|
transform_set_rotation_quat: transform_set_rotation_quat,
|
|
transform_set_scale: transform_set_scale,
|
|
transform_set_matrix: transform_set_matrix,
|
|
transform_get_local_matrix: transform_get_local_matrix,
|
|
transform_get_world_matrix: transform_get_world_matrix,
|
|
|
|
camera_look_at: camera_look_at,
|
|
camera_perspective: camera_perspective,
|
|
camera_ortho: camera_ortho,
|
|
camera_get: camera_get,
|
|
|
|
push_state: push_state,
|
|
pop_state: pop_state,
|
|
|
|
make_material: make_material,
|
|
set_material: set_material,
|
|
set_ambient: set_ambient,
|
|
set_light_dir: set_light_dir,
|
|
set_fog: set_fog,
|
|
|
|
clear: clear,
|
|
clear_depth: clear_depth,
|
|
draw_model: draw_model,
|
|
begin_triangles: begin_triangles,
|
|
begin_lines: begin_lines,
|
|
begin_points: begin_points,
|
|
color: color,
|
|
vertex: vertex,
|
|
end: end,
|
|
|
|
anim_instance: anim_instance,
|
|
anim_clip_count: anim_clip_count,
|
|
anim_clip_duration: anim_clip_duration,
|
|
anim_clip_name: anim_clip_name,
|
|
anim_find_clip: anim_find_clip,
|
|
anim_play: anim_play,
|
|
anim_stop: anim_stop,
|
|
anim_set_time: anim_set_time,
|
|
anim_set_speed: anim_set_speed,
|
|
anim_update: anim_update,
|
|
anim_apply: anim_apply,
|
|
anim_pop_events: anim_pop_events,
|
|
|
|
skin_get: skin_get,
|
|
skin_build_palette: skin_build_palette,
|
|
skin_get_joint_world: skin_get_joint_world,
|
|
skin_find_joint: skin_find_joint,
|
|
skin_attachment_transform: skin_attachment_transform,
|
|
|
|
add_collider_sphere: add_collider_sphere,
|
|
add_collider_box: add_collider_box,
|
|
remove_collider: remove_collider,
|
|
overlaps: overlaps,
|
|
raycast: raycast,
|
|
|
|
btn: btn,
|
|
btnp: btnp,
|
|
axis: axis,
|
|
|
|
seed: seed,
|
|
rand: rand,
|
|
irand: irand,
|
|
|
|
// Internal functions for cart runner
|
|
_state: _state,
|
|
_begin_frame: _begin_frame,
|
|
_process_events: _process_events,
|
|
_end_frame: _end_frame
|
|
} |