826 lines
24 KiB
Plaintext
826 lines
24 KiB
Plaintext
// fx_graph.cm - Compositing graph for rendering
|
|
//
|
|
// Core node types (minimal, generic primitives):
|
|
//
|
|
// render_view
|
|
// Render a scene root with a camera into a target.
|
|
// Params:
|
|
// root - Scene tree root node (or array of drawables)
|
|
// camera - Camera object with pos, width, height, anchor, etc.
|
|
// target - Target spec: {width, height} or 'screen' or existing target
|
|
// clear_color - Optional RGBA clear color, null = no clear
|
|
// Output: {target, commands}
|
|
//
|
|
// composite
|
|
// Combine two inputs with a blend mode.
|
|
// Params:
|
|
// base - Base layer (output from another node)
|
|
// overlay - Overlay layer (output from another node)
|
|
// mode - 'over' (default), 'add', 'multiply'
|
|
// opacity - 0-1, overlay opacity
|
|
// Output: {target, commands}
|
|
//
|
|
// mask
|
|
// Apply a mask to content.
|
|
// Params:
|
|
// content - Content to mask (output from another node)
|
|
// mask - Mask source (output from another node)
|
|
// mode - 'binary' | 'alpha' (default 'alpha')
|
|
// invert - bool, invert mask
|
|
// Output: {target, commands}
|
|
//
|
|
// clip_rect
|
|
// Clip/scissor to a rectangle.
|
|
// Params:
|
|
// input - Input to clip
|
|
// rect - {x, y, width, height} in target coords
|
|
// Output: {target, commands} (same target, adds scissor command)
|
|
//
|
|
// blit
|
|
// Copy/scale an image into a target.
|
|
// Params:
|
|
// input - Source (output from another node or texture)
|
|
// target - Destination target spec or 'screen'
|
|
// dst_rect - {x, y, width, height} destination rectangle
|
|
// filter - 'nearest' | 'linear'
|
|
// Output: {target, commands}
|
|
//
|
|
// present
|
|
// Present a chosen image to the display.
|
|
// Params:
|
|
// input - Final image to present
|
|
// Output: {commands} (no target, just present command)
|
|
//
|
|
// Optimization notes:
|
|
// - Nodes track whether they need an offscreen target or can render directly
|
|
// - render_view to 'screen' skips intermediate target
|
|
// - Sequential composites can be merged when possible
|
|
// - mask uses stencil when available, falls back to RT+sample
|
|
|
|
var fx_graph = {}
|
|
|
|
fx_graph.add_node = function(type, params) {
|
|
params = params || {}
|
|
var node = {
|
|
id: this.next_id++,
|
|
type: type,
|
|
params: params,
|
|
output: {node_id: this.next_id - 1, slot: 'output'}
|
|
}
|
|
this.nodes.push(node)
|
|
return node
|
|
}
|
|
|
|
fx_graph.set_output = function(output_handle) {
|
|
this.output_node = output_handle
|
|
}
|
|
|
|
// Execute graph using backend
|
|
fx_graph.execute = function(backend) {
|
|
var sorted = this.topological_sort()
|
|
var node_outputs = {}
|
|
var all_commands = []
|
|
|
|
for (var node of sorted) {
|
|
var executor = NODE_EXECUTORS[node.type]
|
|
if (!executor) {
|
|
log.console(`fx_graph: No executor for node type: ${node.type}`)
|
|
continue
|
|
}
|
|
|
|
var resolved_params = this.resolve_inputs(node.params, node_outputs)
|
|
resolved_params._node_id = node.id
|
|
|
|
var result = executor(resolved_params, backend)
|
|
node_outputs[node.id] = result
|
|
|
|
// Collect commands from this node
|
|
if (result && result.commands) {
|
|
for (var cmd of result.commands) {
|
|
all_commands.push(cmd)
|
|
}
|
|
}
|
|
}
|
|
|
|
return {commands: all_commands}
|
|
}
|
|
|
|
fx_graph.resolve_inputs = function(params, node_outputs) {
|
|
var resolved = {}
|
|
for (var key in params) {
|
|
var value = params[key]
|
|
if (value && value.node_id != null)
|
|
resolved[key] = node_outputs[value.node_id]
|
|
else
|
|
resolved[key] = value
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
fx_graph.topological_sort = function() {
|
|
// Nodes are added in dependency order by construction - you can't reference
|
|
// a node's output before the node is created. So insertion order is already
|
|
// a valid topological order.
|
|
return this.nodes
|
|
}
|
|
|
|
// ========================================================================
|
|
// NODE EXECUTORS
|
|
// ========================================================================
|
|
|
|
var NODE_EXECUTORS = {}
|
|
|
|
// render_view: Render scene tree to target
|
|
NODE_EXECUTORS.render_view = function(params, backend) {
|
|
var root = params.root
|
|
var camera = params.camera
|
|
var target_spec = params.target
|
|
var clear_color = params.clear_color
|
|
|
|
// Determine if we need an offscreen target
|
|
var needs_offscreen = target_spec != 'screen' && params._needs_offscreen != false
|
|
|
|
var target
|
|
if (target_spec == 'screen') {
|
|
target = 'screen'
|
|
} else if (target_spec && target_spec.texture) {
|
|
// Reuse existing target
|
|
target = target_spec
|
|
} else {
|
|
// Allocate render target
|
|
target = backend.get_or_create_target(
|
|
target_spec.width,
|
|
target_spec.height,
|
|
'view_' + params._node_id
|
|
)
|
|
}
|
|
|
|
// Collect drawables from scene tree
|
|
var drawables = collect_drawables(root, camera)
|
|
|
|
// Sort by layer, then by Y for depth sorting
|
|
drawables.sort((a, b) => {
|
|
return 0
|
|
if (a.layer != b.layer) return a.layer - b.layer
|
|
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 batch of batches) {
|
|
// Emit scissor command 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}
|
|
}
|
|
|
|
// composite: Combine two layers
|
|
NODE_EXECUTORS.composite = function(params, backend) {
|
|
var base = params.base
|
|
var overlay = params.overlay
|
|
var mode = params.mode || 'over'
|
|
var opacity = params.opacity != null ? params.opacity : 1
|
|
|
|
// Optimization: if overlay opacity is 0, just return base
|
|
if (opacity == 0) return base
|
|
|
|
// Optimization: if base is null/empty, just return overlay
|
|
if (!base || !base.target) return overlay
|
|
|
|
// Optimization: if overlay is null/empty, just return base
|
|
if (!overlay || !overlay.target) return base
|
|
|
|
var target = backend.get_or_create_target(
|
|
base.target.width,
|
|
base.target.height,
|
|
'composite_' + params._node_id
|
|
)
|
|
|
|
// Emit composite_textures command (handled outside render pass)
|
|
var commands = []
|
|
commands.push({
|
|
cmd: 'composite_textures',
|
|
base: base.target,
|
|
overlay: overlay.target,
|
|
output: target,
|
|
mode: mode,
|
|
opacity: opacity
|
|
})
|
|
|
|
return {target: target, commands: commands}
|
|
}
|
|
|
|
// mask: Apply mask to content
|
|
NODE_EXECUTORS.mask = function(params, backend) {
|
|
var content = params.content
|
|
var mask = params.mask
|
|
var mode = params.mode || 'alpha'
|
|
var invert = params.invert || false
|
|
|
|
if (!content || !content.target) return {target: null, commands: []}
|
|
if (!mask || !mask.target) return content
|
|
|
|
var target = backend.get_or_create_target(
|
|
content.target.width,
|
|
content.target.height,
|
|
'mask_' + params._node_id
|
|
)
|
|
|
|
// Emit apply_mask command (handled via shader pass outside render pass)
|
|
var commands = []
|
|
commands.push({
|
|
cmd: 'apply_mask',
|
|
content_texture: content.target,
|
|
mask_texture: mask.target,
|
|
output: target,
|
|
mode: mode,
|
|
invert: invert
|
|
})
|
|
|
|
return {target: target, commands: commands}
|
|
}
|
|
|
|
// clip_rect: Apply scissor clipping
|
|
NODE_EXECUTORS.clip_rect = function(params, backend) {
|
|
var input = params.input
|
|
var rect = params.rect
|
|
|
|
if (!input) return {target: null, commands: []}
|
|
|
|
// Clip doesn't need a new target, just adds scissor to commands
|
|
var commands = input.commands ? input.commands.slice() : []
|
|
|
|
// Insert scissor after begin_render
|
|
var insert_idx = 0
|
|
for (var i = 0; i < commands.length; i++) {
|
|
if (commands[i].cmd == 'begin_render') {
|
|
insert_idx = i + 1
|
|
break
|
|
}
|
|
}
|
|
|
|
commands.splice(insert_idx, 0, {cmd: 'scissor', rect: rect})
|
|
|
|
// Add scissor reset before end_render
|
|
for (var i = commands.length - 1; i >= 0; i--) {
|
|
if (commands[i].cmd == 'end_render') {
|
|
commands.splice(i, 0, {cmd: 'scissor', rect: null})
|
|
break
|
|
}
|
|
}
|
|
|
|
return {target: input.target, commands: commands}
|
|
}
|
|
|
|
// blit: Copy/scale image to target
|
|
NODE_EXECUTORS.blit = function(params, backend) {
|
|
var input = params.input
|
|
var target_spec = params.target
|
|
var dst_rect = params.dst_rect
|
|
var filter = params.filter || 'nearest'
|
|
|
|
var src_target = input && input.target ? input.target : input
|
|
if (!src_target) return {target: null, commands: []}
|
|
|
|
var target
|
|
if (target_spec == 'screen') {
|
|
target = 'screen'
|
|
} else if (target_spec && target_spec.target) {
|
|
// Output reference from another node - use its target
|
|
target = target_spec.target
|
|
} else if (target_spec && target_spec.texture) {
|
|
// Already a render target
|
|
target = target_spec
|
|
} else if (target_spec && target_spec.width) {
|
|
// Target spec - use a consistent key based on the spec itself
|
|
var key = `blit_${target_spec.width}x${target_spec.height}`
|
|
target = backend.get_or_create_target(target_spec.width, target_spec.height, key)
|
|
} else {
|
|
return {target: null, commands: []}
|
|
}
|
|
|
|
var commands = []
|
|
commands.push({
|
|
cmd: 'blit',
|
|
texture: src_target,
|
|
target: target,
|
|
dst_rect: dst_rect,
|
|
filter: filter
|
|
})
|
|
|
|
return {target: target, commands: commands}
|
|
}
|
|
|
|
// present: Present to display
|
|
NODE_EXECUTORS.present = function(params, backend) {
|
|
var input = params.input
|
|
|
|
var commands = []
|
|
commands.push({cmd: 'present'})
|
|
|
|
return {commands: commands}
|
|
}
|
|
|
|
// shader_pass: Generic shader pass
|
|
NODE_EXECUTORS.shader_pass = function(params, backend) {
|
|
var input = params.input
|
|
var shader = params.shader
|
|
var uniforms = params.uniforms || {}
|
|
var output_spec = params.output
|
|
|
|
if (!input || !input.target) return {target: null, commands: []}
|
|
|
|
var src = input.target
|
|
var target
|
|
|
|
if (output_spec == 'screen') {
|
|
target = 'screen'
|
|
} else if (output_spec && output_spec.texture) {
|
|
target = output_spec
|
|
} else {
|
|
// Default to input size if not specified
|
|
var w = output_spec && output_spec.width ? output_spec.width : src.width
|
|
var h = output_spec && output_spec.height ? output_spec.height : src.height
|
|
target = backend.get_or_create_target(w, h, 'shader_' + shader + '_' + params._node_id)
|
|
}
|
|
|
|
var commands = []
|
|
commands.push({
|
|
cmd: 'shader_pass',
|
|
shader: shader,
|
|
input: src,
|
|
output: target,
|
|
uniforms: uniforms
|
|
})
|
|
|
|
return {target: target, commands: commands}
|
|
}
|
|
|
|
// ========================================================================
|
|
// SCENE TREE TRAVERSAL
|
|
// ========================================================================
|
|
|
|
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
|
|
|
|
var drawables = []
|
|
|
|
// Compute absolute position
|
|
parent_pos = parent_pos || {x: 0, y: 0}
|
|
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))
|
|
// For recursive calls, use this node's absolute pos as parent pos
|
|
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) {
|
|
// Intersect parent and node 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)) {
|
|
if (node.slice && node.tile) {
|
|
throw Error('Sprite cannot have both "slice" and "tile" parameters.')
|
|
}
|
|
|
|
var px = abs_x
|
|
var py = abs_y
|
|
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)
|
|
|
|
// Helper to add a sprite drawable
|
|
function add_sprite_drawable(rect, uv) {
|
|
drawables.push({
|
|
type: 'sprite',
|
|
layer: node.layer || 0,
|
|
world_y: py,
|
|
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
|
|
})
|
|
}
|
|
|
|
// Helper to emit tiled area
|
|
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_drawable({x: rect.x + ix * tx, y: rect.y + iy * ty, width: qw, height: qh}, quv)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Top-left of whole sprite
|
|
var x0 = px - w * ax
|
|
var y0 = py - h * ay
|
|
|
|
if (node.slice) {
|
|
// 9-Slice logic
|
|
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
|
|
|
|
// World sizes of borders
|
|
var WL = L * Sx
|
|
var WR = R * Sx
|
|
var HT = T * Sy
|
|
var HB = B * Sy
|
|
|
|
// Middle areas
|
|
var WM = w - WL - WR
|
|
var HM = h - HT - HB
|
|
|
|
// UV mid dimensions
|
|
var UM = 1 - L - R
|
|
var VM = 1 - T - B
|
|
|
|
// Natural tile sizes for middle parts
|
|
var TW = stretch != null ? UM * Sx : WM
|
|
var TH = stretch != null ? VM * Sy : HM
|
|
|
|
// TL
|
|
add_sprite_drawable({x: x0, y: y0, width: WL, height: HT}, {x:0, y:0, width: L, height: T})
|
|
// TM
|
|
emit_tiled({x: x0 + WL, y: y0, width: WM, height: HT}, {x:L, y:0, width: UM, height: T}, {x: TW, y: HT})
|
|
// TR
|
|
add_sprite_drawable({x: x0 + WL + WM, y: y0, width: WR, height: HT}, {x: 1-R, y:0, width: R, height: T})
|
|
|
|
// ML
|
|
emit_tiled({x: x0, y: y0 + HT, width: WL, height: HM}, {x:0, y:T, width: L, height: VM}, {x: WL, y: TH})
|
|
// MM
|
|
emit_tiled({x: x0 + WL, y: y0 + HT, width: WM, height: HM}, {x:L, y:T, width: UM, height: VM}, {x: TW, y: TH})
|
|
// MR
|
|
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
|
|
add_sprite_drawable({x: x0, y: y0 + HT + HM, width: WL, height: HB}, {x:0, y: 1-B, width: L, height: B})
|
|
// BM
|
|
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})
|
|
// BR
|
|
add_sprite_drawable({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) {
|
|
// Full sprite tiling
|
|
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: py,
|
|
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
|
|
})
|
|
}
|
|
}
|
|
|
|
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, // 'bitmap', 'sdf', or 'msdf'
|
|
sdf: node.sdf, // legacy support
|
|
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 p of particles) {
|
|
// Particles usually relative to emitter (node) pos
|
|
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, // Sort by Y
|
|
pos: {x: abs_x + px, y: abs_y + py}, // Add parent/node pos to particle pos
|
|
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) {
|
|
// Tilemap emits multiple sprites
|
|
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
|
|
|
|
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
|
|
|
|
// Tile coords are strictly grid based + offset.
|
|
// We should add this node's position (abs_x, abs_y) to it
|
|
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,
|
|
anchor_y: 0,
|
|
color: tint_to_color(world_tint, world_opacity),
|
|
material: node.material,
|
|
scissor: current_scissor
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recurse children
|
|
if (node.children) {
|
|
for (var child of node.children) {
|
|
var child_drawables = collect_drawables(child, camera, world_tint, world_opacity, current_scissor, current_pos)
|
|
drawables = drawables.concat(child_drawables)
|
|
}
|
|
}
|
|
|
|
return drawables
|
|
}
|
|
|
|
function tint_to_color(tint, opacity) {
|
|
return {
|
|
r: tint[0],
|
|
g: tint[1],
|
|
b: tint[2],
|
|
a: tint[3] * opacity
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// BATCHING
|
|
// ========================================================================
|
|
|
|
function batch_drawables(drawables) {
|
|
var batches = []
|
|
var current_batch = null
|
|
|
|
for (var drawable of drawables) {
|
|
if (drawable.type == 'sprite') {
|
|
var texture = drawable.texture || drawable.image
|
|
var material = drawable.material || {blend: 'alpha', sampler: 'nearest'}
|
|
var scissor = drawable.scissor
|
|
|
|
// Start new batch if texture/material/scissor changed
|
|
if (!current_batch ||
|
|
current_batch.type != 'sprite_batch' ||
|
|
current_batch.texture != texture ||
|
|
!rect_equal(current_batch.scissor, scissor) ||
|
|
!materials_equal(current_batch.material, material)) {
|
|
if (current_batch) batches.push(current_batch)
|
|
current_batch = {
|
|
type: 'sprite_batch',
|
|
texture: texture,
|
|
material: material,
|
|
scissor: scissor,
|
|
sprites: []
|
|
}
|
|
}
|
|
|
|
current_batch.sprites.push(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
|
|
}
|
|
|
|
function sprites_to_geometry(sprites) {
|
|
var vertices = []
|
|
var indices = []
|
|
var vertex_count = 0
|
|
|
|
for (var s of sprites) {
|
|
var px = s.pos.x != null ? s.pos.x : (s.pos[0] || 0)
|
|
var py = s.pos.y != null ? s.pos.y : (s.pos[1] || 0)
|
|
var w = s.width || 1
|
|
var h = s.height || 1
|
|
var ax = s.anchor_x || 0
|
|
var ay = s.anchor_y || 0
|
|
var c = s.color || {r: 1, g: 1, b: 1, a: 1}
|
|
|
|
// Apply anchor offset
|
|
var x = px - w * ax
|
|
var y = py - h * ay
|
|
|
|
// Quad vertices (pos, uv, color)
|
|
vertices.push(
|
|
{pos: [x, y], uv: [0, 0], color: c},
|
|
{pos: [x + w, y], uv: [1, 0], color: c},
|
|
{pos: [x + w, y + h], uv: [1, 1], color: c},
|
|
{pos: [x, y + h], uv: [0, 1], color: c}
|
|
)
|
|
|
|
// Two triangles
|
|
indices.push(
|
|
vertex_count, vertex_count + 1, vertex_count + 2,
|
|
vertex_count, vertex_count + 2, vertex_count + 3
|
|
)
|
|
vertex_count += 4
|
|
}
|
|
|
|
return {vertices: vertices, indices: indices}
|
|
}
|
|
|
|
// ========================================================================
|
|
// UTILITY: Fit rectangle to screen with aspect preservation
|
|
// ========================================================================
|
|
|
|
function fit_to_screen(target_spec, window_size) {
|
|
var src_aspect = target_spec.width / target_spec.height
|
|
var dst_aspect = window_size.width / window_size.height
|
|
|
|
var scale = src_aspect > dst_aspect
|
|
? window_size.width / target_spec.width
|
|
: window_size.height / target_spec.height
|
|
|
|
var w = target_spec.width * scale
|
|
var h = target_spec.height * scale
|
|
var x = (window_size.width - w) / 2
|
|
var y = (window_size.height - h) / 2
|
|
|
|
return {x: x, y: y, width: w, height: h}
|
|
}
|
|
|
|
// Export fit_to_screen for external use
|
|
fx_graph.fit_to_screen = fit_to_screen
|
|
function make_fxgraph() {
|
|
return meme(fx_graph, {
|
|
nodes: [],
|
|
output_node: null,
|
|
next_id: 0
|
|
})
|
|
}
|
|
|
|
make_fxgraph.fit_to_screen = fit_to_screen
|
|
|
|
return make_fxgraph
|