Files
prosperon/film2d.cm
2025-12-29 20:46:42 -06:00

449 lines
14 KiB
Plaintext

// film2d.cm - 2D Scene Renderer (Rewritten)
//
// Handles scene tree traversal, sorting, batching, and draw command emission.
// This is the "how to draw a 2D plate" module.
//
// The compositor calls this renderer with (root, camera, target, target_size)
// and it produces draw commands.
//
// This module does NOT handle effects - that's compositor territory.
// It only knows about sprites, tilemaps, text, particles, and rects.
var film2d = {}
// Renderer capabilities
film2d.capabilities = {
supports_mask_stencil: true,
supports_mask_alpha: true,
supports_bloom: true,
supports_blur: true
}
// Main render function
film2d.render = function(params, backend) {
var root = params.root
var camera = params.camera
var target = params.target
var target_size = params.target_size
var clear_color = params.clear
if (!root) return {commands: []}
// Collect all drawables from scene tree
var drawables = _collect_drawables(root, camera, null, null, null, null)
// Sort by layer, then by Y for depth sorting
log.console(drawables.length)
drawables.sort(function(a, b) {
var difflayer = a.layer - b.layer
if (difflayer != 0) return difflayer
return b.world_y - a.world_y
})
// Build render commands
var commands = []
commands.push({cmd: 'begin_render', target: target, clear: clear_color})
commands.push({cmd: 'set_camera', camera: camera})
// Batch and emit draw commands
var batches = _batch_drawables(drawables)
var current_scissor = null
for (var i = 0; i < batches.length; i++) {
var batch = batches[i]
// Emit scissor if changed
if (!_rect_equal(current_scissor, batch.scissor)) {
commands.push({cmd: 'scissor', rect: batch.scissor})
current_scissor = batch.scissor
}
if (batch.type == 'sprite_batch') {
commands.push({
cmd: 'draw_batch',
batch_type: 'sprites',
geometry: {sprites: batch.sprites},
texture: batch.texture,
material: batch.material
})
} else if (batch.type == 'text') {
commands.push({
cmd: 'draw_text',
drawable: batch.drawable
})
} else if (batch.type == 'rect') {
commands.push({
cmd: 'draw_rect',
drawable: batch.drawable
})
} else if (batch.type == 'particles') {
commands.push({
cmd: 'draw_batch',
batch_type: 'particles',
geometry: {sprites: batch.sprites},
texture: batch.texture,
material: batch.material
})
}
}
commands.push({cmd: 'end_render'})
return {target: target, commands: commands}
}
// Collect drawables from scene tree
function _collect_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos) {
if (!node) return []
parent_tint = parent_tint || [1, 1, 1, 1]
parent_opacity = parent_opacity != null ? parent_opacity : 1
parent_pos = parent_pos || {x: 0, y: 0}
var drawables = []
// Compute absolute position
var node_pos = node.pos || {x: 0, y: 0}
var abs_x = parent_pos.x + (node_pos.x != null ? node_pos.x : node_pos[0] || 0)
var abs_y = parent_pos.y + (node_pos.y != null ? node_pos.y : node_pos[1] || 0)
var current_pos = {x: abs_x, y: abs_y}
// Compute inherited tint/opacity
var node_tint = node.tint || node.color
var world_tint = [
parent_tint[0] * (node_tint ? (node_tint.r != null ? node_tint.r : node_tint[0] || 1) : 1),
parent_tint[1] * (node_tint ? (node_tint.g != null ? node_tint.g : node_tint[1] || 1) : 1),
parent_tint[2] * (node_tint ? (node_tint.b != null ? node_tint.b : node_tint[2] || 1) : 1),
parent_tint[3] * (node_tint ? (node_tint.a != null ? node_tint.a : node_tint[3] || 1) : 1)
]
var world_opacity = parent_opacity * (node.opacity != null ? node.opacity : 1)
// Compute effective scissor
var current_scissor = parent_scissor
if (node.scissor) {
if (parent_scissor) {
var x1 = Math.max(parent_scissor.x, node.scissor.x)
var y1 = Math.max(parent_scissor.y, node.scissor.y)
var x2 = Math.min(parent_scissor.x + parent_scissor.width, node.scissor.x + node.scissor.width)
var y2 = Math.min(parent_scissor.y + parent_scissor.height, node.scissor.y + node.scissor.height)
current_scissor = {x: x1, y: y1, width: Math.max(0, x2 - x1), height: Math.max(0, y2 - y1)}
} else {
current_scissor = node.scissor
}
}
// Handle different node types
if (node.type == 'sprite' || (node.image && !node.type)) {
var sprite_drawables = _collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor)
for (var i = 0; i < sprite_drawables.length; i++) {
drawables.push(sprite_drawables[i])
}
}
if (node.type == 'text') {
drawables.push({
type: 'text',
layer: node.layer || 0,
world_y: abs_y,
pos: {x: abs_x, y: abs_y},
text: node.text,
font: node.font,
size: node.size,
mode: node.mode,
sdf: node.sdf,
outline_width: node.outline_width,
outline_color: node.outline_color,
anchor_x: node.anchor_x,
anchor_y: node.anchor_y,
color: _tint_to_color(world_tint, world_opacity),
scissor: current_scissor
})
}
if (node.type == 'rect') {
drawables.push({
type: 'rect',
layer: node.layer || 0,
world_y: abs_y,
pos: {x: abs_x, y: abs_y},
width: node.width || 1,
height: node.height || 1,
color: _tint_to_color(world_tint, world_opacity),
scissor: current_scissor
})
}
if (node.type == 'particles' || node.particles) {
var particles = node.particles || []
for (var i = 0; i < particles.length; i++) {
var p = particles[i]
var px = p.pos ? p.pos.x : 0
var py = p.pos ? p.pos.y : 0
drawables.push({
type: 'sprite',
layer: node.layer || 0,
world_y: abs_y + py,
pos: {x: abs_x + px, y: abs_y + py},
image: node.image,
texture: node.texture,
width: (node.width || 1) * (p.scale || 1),
height: (node.height || 1) * (p.scale || 1),
anchor_x: 0.5,
anchor_y: 0.5,
color: p.color || _tint_to_color(world_tint, world_opacity),
material: node.material,
scissor: current_scissor
})
}
}
if (node.type == 'tilemap' || node.tiles) {
var tile_drawables = _collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor)
for (var i = 0; i < tile_drawables.length; i++) {
drawables.push(tile_drawables[i])
}
}
// Recurse children
if (node.children) {
for (var i = 0; i < node.children.length; i++) {
var child_drawables = _collect_drawables(node.children[i], camera, world_tint, world_opacity, current_scissor, current_pos)
for (var j = 0; j < child_drawables.length; j++) {
drawables.push(child_drawables[j])
}
}
}
return drawables
}
// Collect sprite drawables (handles slice and tile modes)
function _collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) {
var drawables = []
if (node.slice && node.tile) {
throw Error('Sprite cannot have both "slice" and "tile" parameters.')
}
var w = node.width || 1
var h = node.height || 1
var ax = node.anchor_x || 0
var ay = node.anchor_y || 0
var tint = _tint_to_color(world_tint, world_opacity)
function add_sprite(rect, uv) {
drawables.push({
type: 'sprite',
layer: node.layer || 0,
world_y: abs_y,
pos: {x: rect.x, y: rect.y},
image: node.image,
texture: node.texture,
width: rect.width,
height: rect.height,
anchor_x: 0,
anchor_y: 0,
uv_rect: uv,
color: tint,
material: node.material,
scissor: current_scissor
})
}
function emit_tiled(rect, uv, tile_size) {
var tx = tile_size ? (tile_size.x || tile_size) : rect.width
var ty = tile_size ? (tile_size.y || tile_size) : rect.height
var nx = number.ceiling(rect.width / tx - 0.00001)
var ny = number.ceiling(rect.height / ty - 0.00001)
for (var ix = 0; ix < nx; ix++) {
for (var iy = 0; iy < ny; iy++) {
var qw = number.min(tx, rect.width - ix * tx)
var qh = number.min(ty, rect.height - iy * ty)
var quv = {
x: uv.x,
y: uv.y,
width: uv.width * (qw / tx),
height: uv.height * (qh / ty)
}
add_sprite({x: rect.x + ix * tx, y: rect.y + iy * ty, width: qw, height: qh}, quv)
}
}
}
var x0 = abs_x - w * ax
var y0 = abs_y - h * ay
if (node.slice) {
// 9-slice
var s = node.slice
var L = s.left != null ? s.left : (typeof s == 'number' ? s : 0)
var R = s.right != null ? s.right : (typeof s == 'number' ? s : 0)
var T = s.top != null ? s.top : (typeof s == 'number' ? s : 0)
var B = s.bottom != null ? s.bottom : (typeof s == 'number' ? s : 0)
var stretch = s.stretch != null ? s.stretch : node.stretch
var Sx = stretch != null ? (stretch.x || stretch) : w
var Sy = stretch != null ? (stretch.y || stretch) : h
var WL = L * Sx
var WR = R * Sx
var HT = T * Sy
var HB = B * Sy
var WM = w - WL - WR
var HM = h - HT - HB
var UM = 1 - L - R
var VM = 1 - T - B
var TW = stretch != null ? UM * Sx : WM
var TH = stretch != null ? VM * Sy : HM
// TL, TM, TR
add_sprite({x: x0, y: y0, width: WL, height: HT}, {x:0, y:0, width: L, height: T})
emit_tiled({x: x0 + WL, y: y0, width: WM, height: HT}, {x:L, y:0, width: UM, height: T}, {x: TW, y: HT})
add_sprite({x: x0 + WL + WM, y: y0, width: WR, height: HT}, {x: 1-R, y:0, width: R, height: T})
// ML, MM, MR
emit_tiled({x: x0, y: y0 + HT, width: WL, height: HM}, {x:0, y:T, width: L, height: VM}, {x: WL, y: TH})
emit_tiled({x: x0 + WL, y: y0 + HT, width: WM, height: HM}, {x:L, y:T, width: UM, height: VM}, {x: TW, y: TH})
emit_tiled({x: x0 + WL + WM, y: y0 + HT, width: WR, height: HM}, {x: 1-R, y:T, width: R, height: VM}, {x: WR, y: TH})
// BL, BM, BR
add_sprite({x: x0, y: y0 + HT + HM, width: WL, height: HB}, {x:0, y: 1-B, width: L, height: B})
emit_tiled({x: x0 + WL, y: y0 + HT + HM, width: WM, height: HB}, {x:L, y: 1-B, width: UM, height: B}, {x: TW, y: HB})
add_sprite({x: x0 + WL + WM, y: y0 + HT + HM, width: WR, height: HB}, {x: 1-R, y: 1-B, width: R, height: B})
} else if (node.tile) {
emit_tiled({x: x0, y: y0, width: w, height: h}, {x:0, y:0, width: 1, height: 1}, node.tile)
} else {
// Normal sprite
drawables.push({
type: 'sprite',
layer: node.layer || 0,
world_y: abs_y,
pos: {x: abs_x, y: abs_y},
image: node.image,
texture: node.texture,
width: w,
height: h,
anchor_x: ax,
anchor_y: ay,
color: tint,
material: node.material,
scissor: current_scissor
})
}
return drawables
}
// Collect tilemap drawables
function _collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) {
var drawables = []
var tiles = node.tiles || []
var offset_x = node.offset_x || 0
var offset_y = node.offset_y || 0
var scale_x = node.scale_x || 1
var scale_y = node.scale_y || 1
var tint = _tint_to_color(world_tint, world_opacity)
for (var x = 0; x < tiles.length; x++) {
if (!tiles[x]) continue
for (var y = 0; y < tiles[x].length; y++) {
var tile = tiles[x][y]
if (!tile) continue
var world_x = abs_x + (x + offset_x) * scale_x
var world_y_pos = abs_y + (y + offset_y) * scale_y
drawables.push({
type: 'sprite',
layer: node.layer || 0,
world_y: world_y_pos,
pos: {x: world_x, y: world_y_pos},
image: tile,
texture: tile,
width: scale_x,
height: scale_y,
anchor_x: 0,
anchor_y: 0,
color: tint,
material: node.material,
scissor: current_scissor
})
}
}
return drawables
}
function _tint_to_color(tint, opacity) {
return {
r: tint[0],
g: tint[1],
b: tint[2],
a: tint[3] * opacity
}
}
// Batch drawables for efficient rendering
function _batch_drawables(drawables) {
var batches = []
var current_batch = null
var mat = {blend: 'alpha', sampler: 'nearest'}
array.for(drawables, drawable => {
if (drawable.type == 'sprite') {
var texture = drawable.texture || drawable.image
var material = drawable.material || mat
var scissor = drawable.scissor
// Check if can merge with current batch
if (current_batch &&
current_batch.type == 'sprite_batch' &&
current_batch.texture == texture &&
_rect_equal(current_batch.scissor, scissor) &&
_materials_equal(current_batch.material, material)) {
current_batch.sprites.push(drawable)
} else {
if (current_batch) batches.push(current_batch)
current_batch = {
type: 'sprite_batch',
texture: texture,
material: material,
scissor: scissor,
sprites: [drawable]
}
}
} else {
// Non-sprite: flush batch, add individually
if (current_batch) {
batches.push(current_batch)
current_batch = null
}
batches.push({type: drawable.type, drawable: drawable, scissor: drawable.scissor})
}
})
if (current_batch) batches.push(current_batch)
return batches
}
function _rect_equal(a, b) {
if (!a && !b) return true
if (!a || !b) return false
return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height
}
function _materials_equal(a, b) {
if (!a || !b) return a == b
return a.blend == b.blend && a.sampler == b.sampler && a.shader == b.shader
}
return film2d