449 lines
14 KiB
Plaintext
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
|