scene
This commit is contained in:
359
clay2.cm
359
clay2.cm
@@ -1,9 +1,11 @@
|
||||
// clay2.cm - Revised UI layout engine emitting scene trees
|
||||
// clay2.cm - Revised UI layout engine emitting flat drawables
|
||||
//
|
||||
// Changes from clay.cm:
|
||||
// - No __proto__, uses meme/merge
|
||||
// - Emits scene tree nodes for fx_graph instead of immediate draw commands
|
||||
// - Supports scissor clipping on groups
|
||||
// - Emits flat list of drawables for film2d
|
||||
// - Supports scissor clipping
|
||||
//
|
||||
// Now returns [drawable, drawable, ...] instead of {type:'group', ...}
|
||||
|
||||
var layout = use('layout')
|
||||
var graphics = use('graphics')
|
||||
@@ -50,173 +52,6 @@ var root_item
|
||||
var tree_root
|
||||
var config_stack = []
|
||||
|
||||
clay.layout = function(fn, size) {
|
||||
lay_ctx.reset()
|
||||
|
||||
var root = lay_ctx.item()
|
||||
if (isa(size, array)) size = {width: size[0], height: size[1]}
|
||||
|
||||
lay_ctx.set_size(root, size)
|
||||
lay_ctx.set_contain(root, layout.contain.row) // Default root layout
|
||||
|
||||
root_item = root
|
||||
|
||||
var root_config = meme(base_config)
|
||||
tree_root = {
|
||||
id: root,
|
||||
config: root_config,
|
||||
children: []
|
||||
}
|
||||
|
||||
config_stack = [root_config]
|
||||
|
||||
fn()
|
||||
|
||||
lay_ctx.run()
|
||||
|
||||
// Post-layout: build scene tree
|
||||
return build_scene_tree(tree_root, size.height)
|
||||
}
|
||||
|
||||
function build_scene_tree(node, root_height, parent_abs_x, parent_abs_y) {
|
||||
parent_abs_x = parent_abs_x || 0
|
||||
parent_abs_y = parent_abs_y || 0
|
||||
|
||||
var rect = lay_ctx.get_rect(node.id)
|
||||
|
||||
// Calculate absolute world Y for this node (bottom-up layout to top-down render)
|
||||
// rect.y is from bottom
|
||||
var abs_y = root_height - (rect.y + rect.height)
|
||||
var abs_x = rect.x
|
||||
|
||||
// Calculate relative position for the group
|
||||
var rel_x = abs_x - parent_abs_x
|
||||
var rel_y = abs_y - parent_abs_y
|
||||
|
||||
// The node to return. It might be a group or a sprite/text depending on config.
|
||||
var scene_node = {
|
||||
type: 'group',
|
||||
pos: {x: rel_x + node.config.offset.x, y: rel_y + node.config.offset.y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
children: []
|
||||
}
|
||||
|
||||
// Background
|
||||
if (node.config.background_image) {
|
||||
if (node.config.slice) {
|
||||
scene_node.children.push({
|
||||
type: 'sprite',
|
||||
image: node.config.background_image,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
slice: node.config.slice,
|
||||
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
||||
layer: -1 // Back
|
||||
})
|
||||
} else {
|
||||
scene_node.children.push({
|
||||
type: 'sprite',
|
||||
image: node.config.background_image,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
||||
layer: -1
|
||||
})
|
||||
}
|
||||
} else if (node.config.background_color) {
|
||||
scene_node.children.push({
|
||||
type: 'rect',
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.background_color,
|
||||
layer: -1
|
||||
})
|
||||
}
|
||||
|
||||
// Content (Image/Text)
|
||||
if (node.config.image) {
|
||||
scene_node.children.push({
|
||||
type: 'sprite',
|
||||
image: node.config.image,
|
||||
width: rect.width, // layout ensures aspect ratio if configured
|
||||
height: rect.height,
|
||||
color: node.config.color
|
||||
})
|
||||
}
|
||||
|
||||
if (node.config.text) {
|
||||
scene_node.children.push({
|
||||
type: 'text',
|
||||
text: node.config.text,
|
||||
font: node.config.font_path,
|
||||
size: node.config.font_size,
|
||||
color: node.config.color,
|
||||
pos: {x: 0, y: rect.height} // Text baseline relative to group
|
||||
})
|
||||
}
|
||||
|
||||
// Clipping
|
||||
if (node.config.clipped) {
|
||||
// Scissor needs absolute coordinates
|
||||
// We can compute them from our current absolute position
|
||||
scene_node.scissor = {
|
||||
x: abs_x + node.config.offset.x,
|
||||
y: abs_y + node.config.offset.y,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
}
|
||||
|
||||
// Children
|
||||
// Pass our absolute position as the new parent absolute position
|
||||
var my_abs_x = abs_x + node.config.offset.x
|
||||
var my_abs_y = abs_y + node.config.offset.y
|
||||
|
||||
for (var child of node.children) {
|
||||
var child_node = build_scene_tree(child, root_height, my_abs_x, my_abs_y)
|
||||
scene_node.children.push(child_node)
|
||||
}
|
||||
|
||||
return scene_node
|
||||
}
|
||||
|
||||
|
||||
// --- Item Creation Helpers ---
|
||||
|
||||
function add_item(config) {
|
||||
var parent_config = config_stack[config_stack.length-1]
|
||||
|
||||
// Merge parent spacing/padding/margin logic?
|
||||
// clay.cm does normalizing and applying child_gap.
|
||||
|
||||
var use_config = meme(base_config, config)
|
||||
|
||||
var item = lay_ctx.item()
|
||||
lay_ctx.set_margins(item, normalize_spacing(use_config.margin))
|
||||
lay_ctx.set_contain(item, use_config.contain)
|
||||
lay_ctx.set_behave(item, use_config.behave)
|
||||
|
||||
if (use_config.size) {
|
||||
var s = use_config.size
|
||||
if (isa(s, array)) s = {width: s[0], height: s[1]}
|
||||
lay_ctx.set_size(item, s)
|
||||
}
|
||||
|
||||
var node = {
|
||||
id: item,
|
||||
config: use_config,
|
||||
children: []
|
||||
}
|
||||
|
||||
// Link to tree
|
||||
// We need to know current parent node.
|
||||
// Track `tree_stack` alongside `config_stack`?
|
||||
// Or just `tree_node_stack`.
|
||||
|
||||
// Let's fix the state tracking.
|
||||
}
|
||||
|
||||
// Rewriting state management for cleaner recursion
|
||||
var tree_stack = []
|
||||
|
||||
@@ -240,9 +75,149 @@ clay.layout = function(fn, size) {
|
||||
|
||||
lay_ctx.run()
|
||||
|
||||
return build_scene_tree(root_node, size.height)
|
||||
// Post-layout: build flat drawable list
|
||||
return build_drawables(root_node, size.height)
|
||||
}
|
||||
|
||||
function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_scissor, parent_layer) {
|
||||
parent_abs_x = parent_abs_x || 0
|
||||
parent_abs_y = parent_abs_y || 0
|
||||
parent_layer = parent_layer || 0 // UI usually on top, but let's start at 0
|
||||
|
||||
var rect = lay_ctx.get_rect(node.id)
|
||||
|
||||
// Calculate absolute world Y for this node (bottom-up layout to top-down render)
|
||||
var abs_y = root_height - (rect.y + rect.height)
|
||||
var abs_x = rect.x
|
||||
|
||||
// Our absolute position including parent offsets logic from original clay?
|
||||
// Actually layout engine gives rects relative to root usually?
|
||||
// No, layout engine gives RELATIVE logic but `get_rect` usually returns computed layout relative to parent or absolute?
|
||||
// Let's assume `get_rect` returns relative to parent.
|
||||
// Wait, `clay.cm` assumed `rect.x` was absolute?
|
||||
// "Calculate relative position for the group: rel_x = abs_x - parent_abs_x".
|
||||
// This implies `rect.x` is absolute.
|
||||
|
||||
// Let's verify standard behavior. If `layout` returns absolute coords, we don't need to accumulate parent_abs_x for position,
|
||||
// BUT we do need it if `clay` was doing local group offsets.
|
||||
// Original `clay2.cm`: `var rel_x = abs_x - parent_abs_x`. This implies `rect` is absolute.
|
||||
// So `abs_x` IS `rect.x`.
|
||||
|
||||
// IMPORTANT: The offset in config is applied VISUALLY.
|
||||
var vis_x = abs_x + node.config.offset.x
|
||||
var vis_y = abs_y + node.config.offset.y
|
||||
|
||||
var drawables = []
|
||||
|
||||
// Scissor
|
||||
var current_scissor = parent_scissor
|
||||
if (node.config.clipped) {
|
||||
var sx = vis_x
|
||||
var sy = vis_y
|
||||
var sw = rect.width
|
||||
var sh = rect.height
|
||||
|
||||
// Intersect with parent
|
||||
if (parent_scissor) {
|
||||
sx = number.max(sx, parent_scissor.x)
|
||||
sy = number.max(sy, parent_scissor.y)
|
||||
var right = number.min(vis_x + sw, parent_scissor.x + parent_scissor.width)
|
||||
var bottom = number.min(vis_y + sh, parent_scissor.y + parent_scissor.height)
|
||||
sw = number.max(0, right - sx)
|
||||
sh = number.max(0, bottom - sy)
|
||||
}
|
||||
|
||||
current_scissor = {x: sx, y: sy, width: sw, height: sh}
|
||||
}
|
||||
|
||||
// Background
|
||||
if (node.config.background_image) {
|
||||
if (node.config.slice) {
|
||||
drawables.push({
|
||||
type: 'sprite',
|
||||
image: node.config.background_image,
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
slice: node.config.slice,
|
||||
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
||||
layer: parent_layer - 0.1, // slightly behind content
|
||||
scissor: current_scissor
|
||||
})
|
||||
} else {
|
||||
drawables.push({
|
||||
type: 'sprite',
|
||||
image: node.config.background_image,
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
||||
layer: parent_layer - 0.1,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
} else if (node.config.background_color) {
|
||||
drawables.push({
|
||||
type: 'rect',
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.background_color,
|
||||
layer: parent_layer - 0.1,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
|
||||
// Content (Image/Text)
|
||||
if (node.config.image) {
|
||||
drawables.push({
|
||||
type: 'sprite',
|
||||
image: node.config.image,
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.color,
|
||||
layer: parent_layer,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
|
||||
if (node.config.text) {
|
||||
drawables.push({
|
||||
type: 'text',
|
||||
text: node.config.text,
|
||||
font: node.config.font_path,
|
||||
size: node.config.font_size,
|
||||
color: node.config.color,
|
||||
pos: {x: vis_x, y: vis_y + rect.height}, // Baseline adjustment
|
||||
anchor_y: 1.0, // Text usually draws from baseline up or top down?
|
||||
// film2d text uses top-left by default unless anchor set.
|
||||
// Original clay put it at `y + rect.height`.
|
||||
// Let's assume origin top-left, so we might need anchor adjustment or just position.
|
||||
// If frame is top-down (0 at top), `abs_y` is top.
|
||||
// `rect.y` in layout is bottom-up? "rect.y is from bottom" says original comment.
|
||||
// `abs_y = root_height - (rect.y + rect.height)` -> Top edge of element.
|
||||
// Text usually wants baseline.
|
||||
// If we put it at `vis_y + rect.height`, that's bottom of element.
|
||||
layer: parent_layer,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
|
||||
// Children
|
||||
for (var child of node.children) {
|
||||
var child_drawables = build_drawables(child, root_height, vis_x, vis_y, current_scissor, parent_layer + 0.01)
|
||||
for (var i = 0; i < child_drawables.length; i++) {
|
||||
drawables.push(child_drawables[i])
|
||||
}
|
||||
}
|
||||
|
||||
return drawables
|
||||
}
|
||||
|
||||
|
||||
// --- Item Creation Helpers ---
|
||||
|
||||
function process_configs(configs) {
|
||||
return meme(base_config, ...configs)
|
||||
}
|
||||
@@ -299,8 +274,6 @@ clay.vstack = function(configs, fn) {
|
||||
if (!isa(configs, array)) configs = [configs]
|
||||
|
||||
var c = layout.contain.column
|
||||
// Check for alignment/justification in configs?
|
||||
// Assume generic container props handle it via `contain` override or we defaults
|
||||
|
||||
push_node(configs, c)
|
||||
if (fn) fn()
|
||||
@@ -321,11 +294,7 @@ clay.zstack = function(configs, fn) {
|
||||
if (typeof configs == 'function') { fn = configs; configs = {} }
|
||||
if (!isa(configs, array)) configs = [configs]
|
||||
|
||||
// Stack (overlap)
|
||||
// layout.contain.stack? layout.contain.overlap?
|
||||
// 'layout' module usually defaults to overlap if not row/column?
|
||||
// Or we just don't set row/column bit.
|
||||
var c = layout.contain.layout // Just layout (no flow)
|
||||
var c = layout.contain.layout
|
||||
|
||||
push_node(configs, c)
|
||||
if (fn) fn()
|
||||
@@ -336,10 +305,8 @@ clay.zstack = function(configs, fn) {
|
||||
clay.image = function(path, ...configs) {
|
||||
var img = graphics.texture(path)
|
||||
var c = {image: path}
|
||||
// Auto-size if not provided
|
||||
// But we need to check if configs override it
|
||||
var final_config = process_configs(configs)
|
||||
if (!final_config.size && !final_config.behave) { // If no size and no fill behavior
|
||||
if (!final_config.size && !final_config.behave) {
|
||||
c.size = {width: img.width, height: img.height}
|
||||
}
|
||||
|
||||
@@ -349,32 +316,9 @@ clay.image = function(path, ...configs) {
|
||||
|
||||
clay.text = function(str, ...configs) {
|
||||
var c = {text: str}
|
||||
// Measuring
|
||||
var final_config = process_configs(configs)
|
||||
// measure text
|
||||
var font = graphics.get_font(final_config.font_path) // or font cache
|
||||
// Need to handle font object vs path string
|
||||
// For now assume path string in config or default
|
||||
|
||||
// This measurement is synchronous and might differ from GPU font rendering slightly
|
||||
// but good enough for layout.
|
||||
// We need to know max width for wrapping?
|
||||
// 'layout' doesn't easily support "height depends on width" during single pass?
|
||||
// We specify a fixed size or behave.
|
||||
|
||||
// If no size specified, measure single line
|
||||
if (!final_config.size && !final_config.behave) {
|
||||
// Basic measurement
|
||||
// Hack: use arbitrary width for now?
|
||||
// Or we need a proper text measurement exposed.
|
||||
// graphics.measure_text(font, text, size, break, align)
|
||||
// Assume we have it or minimal version.
|
||||
// clay.cm used `font.text_size`
|
||||
|
||||
// We'll rely on font path to get a font object
|
||||
// var f = graphics.get_font(final_config.font_path) ...
|
||||
// c.size = ...
|
||||
c.size = {width: 100, height: 20} // Fallback for now to avoid crashes
|
||||
c.size = {width: 100, height: 20}
|
||||
}
|
||||
|
||||
push_node([c, ...configs], null)
|
||||
@@ -382,16 +326,11 @@ clay.text = function(str, ...configs) {
|
||||
}
|
||||
|
||||
clay.rectangle = function(...configs) {
|
||||
// Just a container with background color really, but as a leaf
|
||||
push_node(configs, null)
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.button = function(str, action, ...configs) {
|
||||
// Button is a container with text and click behavior
|
||||
// For rendering, it's a zstack of background + text
|
||||
|
||||
// We can just define it as a container with background styling
|
||||
var btn_config = {
|
||||
padding: 10,
|
||||
background_color: {r:0.3, g:0.3, b:0.4, a:1}
|
||||
|
||||
390
compositor.cm
390
compositor.cm
@@ -4,10 +4,12 @@
|
||||
// It takes scene descriptions and produces abstract render plans.
|
||||
//
|
||||
// Architecture:
|
||||
// Scene Tree (Retained) -> Compositor (Effect orchestration) -> Render Plan -> Backend
|
||||
// Composition Tree (Structure) -> Compositor -> Render Plan -> Backend
|
||||
//
|
||||
// The compositor does NOT know about sprites/tilemaps/text - that's renderer territory.
|
||||
// It only knows about plates, targets, viewports, effects, and post-processing.
|
||||
// Changes:
|
||||
// - No longer iterates scene/node trees.
|
||||
// - Relies on 'resolve(ctx)' callbacks to get flat drawable lists.
|
||||
// - Handles groups by filtering drawables and re-injecting texture_refs.
|
||||
|
||||
var effects_mod = use('effects')
|
||||
|
||||
@@ -42,29 +44,30 @@ compositor.compile = function(comp, renderers, backend) {
|
||||
target_size: null,
|
||||
|
||||
// Target allocation
|
||||
alloc_target: function(w, h, hint) {
|
||||
alloc_target: function(width, height, hint) {
|
||||
var key = (hint || 'target') + '_' + text(this.target_counter++)
|
||||
this.targets[key] = {w: w, h: h, key: key}
|
||||
return {type: 'target', key: key, w: w, h: h}
|
||||
this.targets[key] = {width: width, height: height, key: key}
|
||||
return {type: 'target', key: key, width: width, height: height}
|
||||
},
|
||||
|
||||
// Persistent target (survives across frames)
|
||||
get_persistent_target: function(key, width, height) {
|
||||
if (!this.persistent_targets[key]) {
|
||||
this.persistent_targets[key] = {width: width, height: height, key: key, persistent: true}
|
||||
}
|
||||
return {type: 'target', key: key, width: width, height: height, persistent: true}
|
||||
},
|
||||
|
||||
// Persistent target (survives across frames)
|
||||
get_persistent_target: function(key, w, h) {
|
||||
if (!this.persistent_targets[key]) {
|
||||
this.persistent_targets[key] = {w: w, h: h, key: key, persistent: true}
|
||||
}
|
||||
return {type: 'target', key: key, w: w, h: h, persistent: true}
|
||||
// Helper to resolve drawables from a node (layer/plane)
|
||||
resolve_drawables: function(node) {
|
||||
if (node.drawables) return node.drawables
|
||||
if (node.resolve) return node.resolve(this)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Get screen size from backend
|
||||
ctx.screen_size = backend.get_window_size ? backend.get_window_size() : {width: 1280, height: 720}
|
||||
if (!ctx.screen_size.width) ctx.screen_size.width = 1280
|
||||
if (!ctx.screen_size.height) ctx.screen_size.height = 720
|
||||
|
||||
// Normalize to w/h
|
||||
ctx.screen_size.w = ctx.screen_size.width
|
||||
ctx.screen_size.h = ctx.screen_size.height
|
||||
|
||||
// Compile the tree
|
||||
_compile_node(comp, ctx, null, ctx.screen_size)
|
||||
@@ -80,24 +83,24 @@ compositor.compile = function(comp, renderers, backend) {
|
||||
// Compile a single node
|
||||
function _compile_node(node, ctx, parent_target, parent_size) {
|
||||
if (!node) return {output: null}
|
||||
|
||||
|
||||
var node_type = node.type
|
||||
var owns_target = false
|
||||
var target = parent_target
|
||||
var target_size = {w: parent_size.w || parent_size.width, h: parent_size.h || parent_size.height}
|
||||
|
||||
var target_size = {width: parent_size.width, height: parent_size.height}
|
||||
|
||||
// Determine target ownership
|
||||
if (node.target == 'screen') {
|
||||
owns_target = true
|
||||
target = 'screen'
|
||||
target_size = {w: ctx.screen_size.w, h: ctx.screen_size.h}
|
||||
target_size = {width: ctx.screen_size.width, height: ctx.screen_size.height}
|
||||
} else if (node.resolution) {
|
||||
owns_target = true
|
||||
target_size = {w: node.resolution.w || node.resolution.width, h: node.resolution.h || node.resolution.height}
|
||||
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'res_target')
|
||||
target_size = {width: node.resolution.width, height: node.resolution.height}
|
||||
target = ctx.alloc_target(target_size.width, target_size.height, node.name || 'res_target')
|
||||
} else if (node.effects && _effects_require_target(node.effects, ctx)) {
|
||||
owns_target = true
|
||||
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'effect_target')
|
||||
target = ctx.alloc_target(target_size.width, target_size.height, node.name || 'effect_target')
|
||||
}
|
||||
|
||||
ctx.target_size = target_size
|
||||
@@ -115,7 +118,7 @@ function _compile_node(node, ctx, parent_target, parent_size) {
|
||||
if (node_type == 'composition') {
|
||||
return _compile_composition(node, ctx, target, target_size)
|
||||
} else if (node_type == 'group') {
|
||||
return _compile_group(node, ctx, target, target_size, parent_target, parent_size)
|
||||
return _compile_group_layer(node, ctx, target, target_size, parent_target, parent_size)
|
||||
} else if (_is_renderer_type(node_type)) {
|
||||
return _compile_renderer(node, ctx, target, target_size, parent_target, parent_size)
|
||||
} else if (node_type == 'imgui') {
|
||||
@@ -147,8 +150,8 @@ function _compile_composition(node, ctx, target, target_size) {
|
||||
return {output: target}
|
||||
}
|
||||
|
||||
// Compile group with effects
|
||||
function _compile_group(node, ctx, target, target_size, parent_target, parent_size) {
|
||||
// Compile group layer (folder of layers)
|
||||
function _compile_group_layer(node, ctx, target, target_size, parent_target, parent_size) {
|
||||
var layers = node.layers || []
|
||||
var original_target = target
|
||||
|
||||
@@ -161,7 +164,7 @@ function _compile_group(node, ctx, target, target_size, parent_target, parent_si
|
||||
_compile_node(layer, ctx, target, target_size)
|
||||
}
|
||||
|
||||
// Apply effects - this is where the compositor owns all effect logic
|
||||
// Apply effects
|
||||
if (node.effects && node.effects.length > 0) {
|
||||
for (var j = 0; j < node.effects.length; j++) {
|
||||
var effect = node.effects[j]
|
||||
@@ -170,58 +173,32 @@ function _compile_group(node, ctx, target, target_size, parent_target, parent_si
|
||||
}
|
||||
|
||||
// Composite back to parent if needed
|
||||
var needs_composite = (original_target != parent_target || target != original_target) && parent_target
|
||||
if (needs_composite) {
|
||||
var presentation = node.presentation || 'disabled'
|
||||
var blend = node.blend || 'over'
|
||||
|
||||
if (parent_target == 'screen') {
|
||||
ctx.passes.push({
|
||||
type: 'blit_to_screen',
|
||||
source: target,
|
||||
source_size: target_size,
|
||||
dest_size: parent_size,
|
||||
presentation: presentation,
|
||||
pos: node.pos
|
||||
})
|
||||
} else {
|
||||
ctx.passes.push({
|
||||
type: 'composite',
|
||||
source: target,
|
||||
dest: parent_target,
|
||||
source_size: target_size,
|
||||
dest_size: parent_size,
|
||||
presentation: presentation,
|
||||
blend: blend,
|
||||
pos: node.pos
|
||||
})
|
||||
}
|
||||
}
|
||||
_composite_back(ctx, target, target_size, original_target, parent_target, parent_size, node)
|
||||
|
||||
return {output: target}
|
||||
}
|
||||
|
||||
// Compile renderer layer (film2d, forward3d, etc.)
|
||||
// Compile renderer layer (film2d, etc.)
|
||||
function _compile_renderer(node, ctx, target, target_size, parent_target, parent_size) {
|
||||
var renderer_type = node.type
|
||||
var renderer = ctx.renderers[renderer_type]
|
||||
|
||||
|
||||
if (!renderer) {
|
||||
log.console(`compositor: Unknown renderer: ${renderer_type}`)
|
||||
return {output: target}
|
||||
}
|
||||
|
||||
|
||||
var layer_target = target
|
||||
var layer_size = target_size
|
||||
var owns_target = false
|
||||
|
||||
|
||||
// Check for resolution override
|
||||
if (node.resolution) {
|
||||
owns_target = true
|
||||
layer_size = {w: node.resolution.w, h: node.resolution.h}
|
||||
layer_target = ctx.alloc_target(layer_size.w, layer_size.h, node.name || renderer_type + '_target')
|
||||
layer_size = {width: node.resolution.width, height: node.resolution.height}
|
||||
layer_target = ctx.alloc_target(layer_size.width, layer_size.height, node.name || renderer_type + '_target')
|
||||
}
|
||||
|
||||
|
||||
// Clear if we own target
|
||||
if (owns_target && node.clear) {
|
||||
ctx.passes.push({
|
||||
@@ -230,63 +207,227 @@ function _compile_renderer(node, ctx, target, target_size, parent_target, parent
|
||||
color: node.clear
|
||||
})
|
||||
}
|
||||
|
||||
// 1. Resolve ALL drawables for this plane
|
||||
var all_drawables = ctx.resolve_drawables(node)
|
||||
|
||||
// Emit render pass
|
||||
// 2. Identify Group Memberships and Subtractions
|
||||
var groups = node.groups || []
|
||||
var consumed_indices = {} // Set of indices in all_drawables that are consumed
|
||||
|
||||
// If we have groups, we need to process them
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var group = groups[i]
|
||||
var group_drawables = []
|
||||
|
||||
// Resolve group selection
|
||||
if (group.select) {
|
||||
// Selector logic
|
||||
/*
|
||||
Simple selector: { tags: ['tag1'] } or { handles: [...] }
|
||||
*/
|
||||
if (group.select.tags) {
|
||||
var tags = group.select.tags
|
||||
for (var k = 0; k < all_drawables.length; k++) {
|
||||
var d = all_drawables[k]
|
||||
// Check if 'd' has any of the tags
|
||||
// If 'd' is a handle, it has .tags array
|
||||
// If 'd' is a struct, it might have .tags?
|
||||
// Assuming all filterable items are handles for now or have tags prop
|
||||
if (d.tags || (d.has_tag)) {
|
||||
var match = false
|
||||
for (var t = 0; t < tags.length; t++) {
|
||||
if (d.has_tag && d.has_tag(tags[t])) { match = true; break }
|
||||
if (d.tags && d.tags.indexOf && d.tags.indexOf(tags[t]) >= 0) { match = true; break }
|
||||
}
|
||||
if (match) {
|
||||
group_drawables.push(d)
|
||||
consumed_indices[k] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ask group to resolve itself if it has a custom callback?
|
||||
if (group.resolve) {
|
||||
// If group resolves explicit list, we need to match them to base list to remove them
|
||||
// Or just trust the group list and try to remove by reference
|
||||
var gd = group.resolve(ctx)
|
||||
group_drawables = gd
|
||||
// Mark as consumed by reference check? O(N*M) - risky.
|
||||
// Assume ID check if handles.
|
||||
for (var g = 0; g < gd.length; g++) {
|
||||
var item = gd[g]
|
||||
var id = item._id || item.id
|
||||
if (id) {
|
||||
for (var k = 0; k < all_drawables.length; k++) {
|
||||
if (all_drawables[k]._id == id || all_drawables[k].id == id) {
|
||||
consumed_indices[k] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ref equality fallback
|
||||
var idx = all_drawables.indexOf(item)
|
||||
if (idx >= 0) consumed_indices[idx] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render Group
|
||||
if (group_drawables.length > 0) {
|
||||
var group_target = ctx.alloc_target(layer_size.width, layer_size.height, (group.name||'group') + '_content')
|
||||
var group_out = group_target
|
||||
|
||||
// Render content
|
||||
ctx.passes.push({
|
||||
type: 'render',
|
||||
renderer: renderer_type,
|
||||
drawables: group_drawables,
|
||||
camera: node.camera,
|
||||
target: group_target,
|
||||
target_size: layer_size,
|
||||
blend: 'replace',
|
||||
clear: {r:0,g:0,b:0,a:0}
|
||||
})
|
||||
|
||||
// Apply effects
|
||||
if (group.effects) {
|
||||
for (var e = 0; e < group.effects.length; e++) {
|
||||
group_out = _compile_effect(group.effects[e], ctx, group_out, layer_size, group.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert result as texture_ref into MAIN list
|
||||
// We need to inject it. We can add it to 'all_drawables' but mark it as NOT consumed?
|
||||
// No, 'all_drawables' iteration is done.
|
||||
// We'll construct the 'final_drawables' list.
|
||||
|
||||
// Create texture_ref drawable.
|
||||
// Note: we can't create a 'handle' easily here without film2d's help, BUT film2d.render accepts raw structs.
|
||||
var tex_ref = {
|
||||
type: 'texture_ref',
|
||||
texture_target: group_out,
|
||||
pos: {x: 0, y: 0}, // Full screen quad usually? Or group bounds? User: "Ship full-plane first".
|
||||
width: layer_size.width,
|
||||
height: layer_size.height,
|
||||
layer: group.output_layer || 0,
|
||||
blend: 'over', // Textures usually blend over
|
||||
world_y: (group.pos ? group.pos.y : 0) // Sorting? "Group is atomic in ordering... Insert at output_layer"
|
||||
}
|
||||
|
||||
// We defer adding this to the final list until after filtering
|
||||
group.generated_ref = tex_ref
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Construct Final List
|
||||
var final_drawables = []
|
||||
for (var k = 0; k < all_drawables.length; k++) {
|
||||
if (!consumed_indices[k]) {
|
||||
final_drawables.push(all_drawables[k])
|
||||
}
|
||||
}
|
||||
|
||||
// Add generated refs
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
if (groups[i].generated_ref) {
|
||||
final_drawables.push(groups[i].generated_ref)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Main Render Pass
|
||||
ctx.passes.push({
|
||||
type: 'render',
|
||||
renderer: renderer_type,
|
||||
root: node.root,
|
||||
drawables: final_drawables,
|
||||
camera: node.camera,
|
||||
target: layer_target,
|
||||
target_size: layer_size,
|
||||
blend: node.blend || 'over',
|
||||
clear: owns_target ? node.clear : null
|
||||
})
|
||||
|
||||
|
||||
// Composite back to parent
|
||||
if (owns_target && parent_target) {
|
||||
_composite_back(ctx, layer_target, layer_size, target, parent_target, parent_size, node)
|
||||
|
||||
return {output: layer_target}
|
||||
}
|
||||
|
||||
function _composite_back(ctx, current_target, current_size, original_target, parent_target, parent_size, node) {
|
||||
var needs_composite = (original_target != parent_target || current_target != original_target) && parent_target
|
||||
|
||||
if (needs_composite) {
|
||||
var presentation = node.presentation || 'disabled'
|
||||
var blend = node.blend || 'over'
|
||||
|
||||
ctx.passes.push({
|
||||
type: 'composite',
|
||||
source: layer_target,
|
||||
dest: parent_target,
|
||||
source_size: layer_size,
|
||||
dest_size: parent_size,
|
||||
presentation: presentation,
|
||||
blend: blend,
|
||||
pos: node.pos
|
||||
})
|
||||
if (parent_target == 'screen') {
|
||||
ctx.passes.push({
|
||||
type: 'blit_to_screen',
|
||||
source: current_target,
|
||||
source_size: current_size,
|
||||
dest_size: parent_size,
|
||||
presentation: presentation,
|
||||
pos: node.pos
|
||||
})
|
||||
} else {
|
||||
ctx.passes.push({
|
||||
type: 'composite',
|
||||
source: current_target,
|
||||
dest: parent_target,
|
||||
source_size: current_size,
|
||||
dest_size: parent_size,
|
||||
presentation: presentation,
|
||||
blend: blend,
|
||||
pos: node.pos
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {output: layer_target}
|
||||
}
|
||||
|
||||
// Compile an effect using the effect registry
|
||||
function _compile_effect(effect, ctx, input_target, target_size, node_id) {
|
||||
var effect_type = effect.type
|
||||
var effect_def = effects_mod.get(effect_type)
|
||||
|
||||
|
||||
if (!effect_def) {
|
||||
log.console(`compositor: Unknown effect: ${effect_type}`)
|
||||
return input_target
|
||||
}
|
||||
|
||||
|
||||
// Build effect context
|
||||
var effect_ctx = {
|
||||
backend: ctx.backend,
|
||||
target_size: {w: target_size.w, h: target_size.h},
|
||||
alloc_target: function(w, h, hint) {
|
||||
return ctx.alloc_target(w, h, hint)
|
||||
target_size: {width: target_size.width, height: target_size.height},
|
||||
alloc_target: function(width, height, hint) {
|
||||
return ctx.alloc_target(width, height, hint)
|
||||
},
|
||||
get_persistent_target: function(key, w, h) {
|
||||
return ctx.get_persistent_target(node_id + '_' + key, w, h)
|
||||
get_persistent_target: function(key, width, height) {
|
||||
return ctx.get_persistent_target(node_id + '_' + key, width, height)
|
||||
},
|
||||
// Allows effects to resolve sources (for masks)
|
||||
resolve_source: function(source_def) {
|
||||
// If source is "tags", iterate all drawables of the current renderer?
|
||||
// This is tricky. Masks need access to the plane's drawables.
|
||||
// Simplified: The mask effect in 'paladin.ce' has a 'source' handle directly.
|
||||
// We can wrap it in a drawable list.
|
||||
if (source_def.type == 'handle') {
|
||||
return [source_def]
|
||||
}
|
||||
if (source_def.tags) {
|
||||
// We don't have easy access to the full list here unless we passed it.
|
||||
// For now, assume mask sources are explicit handles or handle lists passed in the effect config.
|
||||
return []
|
||||
}
|
||||
// If source is a handle object (has _id)
|
||||
if (source_def._id || source_def.type) return [source_def]
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Allocate output target
|
||||
var output = ctx.alloc_target(target_size.w, target_size.h, effect_type + '_out')
|
||||
var output = ctx.alloc_target(target_size.width, target_size.height, effect_type + '_out')
|
||||
|
||||
// Build effect passes
|
||||
var effect_passes = effect_def.build_passes(input_target, output, effect, effect_ctx)
|
||||
@@ -304,14 +445,19 @@ function _compile_effect(effect, ctx, input_target, target_size, node_id) {
|
||||
function _convert_effect_pass(ep, ctx) {
|
||||
switch (ep.type) {
|
||||
case 'shader':
|
||||
if (!ep.input && !ep.inputs) {
|
||||
ep.input = ep.inputs[0]
|
||||
// Handle both single input and multiple inputs array
|
||||
var primary_input = ep.input
|
||||
var extra = []
|
||||
|
||||
if (is_array(ep.inputs) && ep.inputs.length > 0) {
|
||||
if (!primary_input) primary_input = ep.inputs[0]
|
||||
extra = ep.inputs.slice(1)
|
||||
}
|
||||
return {
|
||||
type: 'shader_pass',
|
||||
shader: ep.shader,
|
||||
input: ep.input || ep.inputs[0],
|
||||
extra_inputs: ep.inputs ? ep.inputs.slice(1) : [],
|
||||
input: primary_input,
|
||||
extra_inputs: extra,
|
||||
output: ep.output,
|
||||
uniforms: ep.uniforms
|
||||
}
|
||||
@@ -328,16 +474,22 @@ function _convert_effect_pass(ep, ctx) {
|
||||
type: 'composite',
|
||||
source: ep.source,
|
||||
dest: ep.dest,
|
||||
source_size: ep.source.w ? {w: ep.source.w, h: ep.source.h} : ctx.target_size,
|
||||
dest_size: ep.dest.w ? {w: ep.dest.w, h: ep.dest.h} : ctx.target_size,
|
||||
source_size: ep.source.width ? {width: ep.source.width, height: ep.source.height} : ctx.target_size,
|
||||
dest_size: ep.dest.width ? {width: ep.dest.width, height: ep.dest.height} : ctx.target_size,
|
||||
presentation: 'disabled'
|
||||
}
|
||||
case 'render_subtree':
|
||||
// This is now "render_drawables"
|
||||
// Ensure drawables is an array because film2d expects array
|
||||
var list = ep.root
|
||||
if (!list) list = []
|
||||
else if (!is_array(list)) list = [list]
|
||||
|
||||
return {
|
||||
type: 'render_mask_source',
|
||||
source: ep.root,
|
||||
drawables: list,
|
||||
target: ep.output,
|
||||
target_size: {w: ep.output.w, h: ep.output.h},
|
||||
target_size: {width: ep.output.width, height: ep.output.height},
|
||||
space: ep.space || 'local'
|
||||
}
|
||||
default:
|
||||
@@ -356,40 +508,40 @@ function _effects_require_target(effects, ctx) {
|
||||
|
||||
// Check if type is a renderer
|
||||
function _is_renderer_type(type) {
|
||||
return type == 'film2d' || type == 'forward3d' || type == 'imgui' || type == 'retro3d' || type == 'simple2d'
|
||||
return type == 'film2d' || type == 'forward3d' || type == 'retro3d' || type == 'simple2d'
|
||||
}
|
||||
|
||||
// Calculate presentation rect
|
||||
compositor.calculate_presentation_rect = function(source_size, dest_size, mode) {
|
||||
var sw = source_size.w || source_size.width
|
||||
var sh = source_size.h || source_size.height
|
||||
var dw = dest_size.w || dest_size.width
|
||||
var dh = dest_size.h || dest_size.height
|
||||
|
||||
var sw = source_size.width
|
||||
var sh = source_size.height
|
||||
var dw = dest_size.width
|
||||
var dh = dest_size.height
|
||||
|
||||
if (mode == 'disabled') {
|
||||
return {x: 0, y: 0, width: number.min(sw, dw), height: number.min(sh, dh)}
|
||||
}
|
||||
if (mode == 'stretch') {
|
||||
return {x: 0, y: 0, width: dw, height: dh}
|
||||
}
|
||||
|
||||
|
||||
var src_aspect = sw / sh
|
||||
var dst_aspect = dw / dh
|
||||
|
||||
|
||||
if (mode == 'letterbox') {
|
||||
var scale = src_aspect > dst_aspect ? dw / sw : dh / sh
|
||||
var w = sw * scale
|
||||
var h = sh * scale
|
||||
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
||||
}
|
||||
|
||||
|
||||
if (mode == 'overscan') {
|
||||
var scale = src_aspect > dst_aspect ? dh / sh : dw / sw
|
||||
var w = sw * scale
|
||||
var h = sh * scale
|
||||
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
||||
}
|
||||
|
||||
|
||||
if (mode == 'integer_scale') {
|
||||
var scale_x = number.floor(dw / sw)
|
||||
var scale_y = number.floor(dh / sh)
|
||||
@@ -398,24 +550,24 @@ compositor.calculate_presentation_rect = function(source_size, dest_size, mode)
|
||||
var h = sh * scale
|
||||
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
||||
}
|
||||
|
||||
|
||||
return {x: 0, y: 0, width: sw, height: sh}
|
||||
}
|
||||
|
||||
// Execute a compiled render plan
|
||||
compositor.execute = function(plan, renderers, backend) {
|
||||
var target_cache = {}
|
||||
|
||||
|
||||
// Pre-allocate targets
|
||||
for (var key in plan.targets) {
|
||||
var spec = plan.targets[key]
|
||||
target_cache[key] = backend.get_or_create_target(spec.w, spec.h, key)
|
||||
target_cache[key] = backend.get_or_create_target(spec.width, spec.height, key)
|
||||
}
|
||||
|
||||
|
||||
for (var key in plan.persistent_targets) {
|
||||
var spec = plan.persistent_targets[key]
|
||||
if (!target_cache[key]) {
|
||||
target_cache[key] = backend.get_or_create_target(spec.w, spec.h, key)
|
||||
target_cache[key] = backend.get_or_create_target(spec.width, spec.height, key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,26 +596,28 @@ compositor.execute = function(plan, renderers, backend) {
|
||||
function _execute_pass(pass, renderers, backend, resolve_target, comp) {
|
||||
var commands = []
|
||||
var source
|
||||
|
||||
|
||||
switch (pass.type) {
|
||||
case 'clear':
|
||||
var target = resolve_target(pass.target)
|
||||
commands.push({cmd: 'begin_render', target: target, clear: pass.color})
|
||||
commands.push({cmd: 'end_render'})
|
||||
break
|
||||
|
||||
|
||||
case 'render':
|
||||
var renderer = renderers[pass.renderer]
|
||||
if (renderer && renderer.render) {
|
||||
var target = resolve_target(pass.target)
|
||||
// Pass drawables directly
|
||||
var result = renderer.render({
|
||||
root: pass.root,
|
||||
drawables: pass.drawables,
|
||||
camera: pass.camera,
|
||||
target: target,
|
||||
target_size: pass.target_size,
|
||||
blend: pass.blend,
|
||||
clear: pass.clear
|
||||
}, backend)
|
||||
|
||||
if (result && result.commands) {
|
||||
for (var i = 0; i < result.commands.length; i++) {
|
||||
commands.push(result.commands[i])
|
||||
@@ -569,11 +723,11 @@ function _execute_pass(pass, renderers, backend, resolve_target, comp) {
|
||||
|
||||
case 'render_mask_source': {
|
||||
var target = resolve_target(pass.target)
|
||||
var renderer = renderers['film2d']
|
||||
var renderer = renderers['film2d'] // Assume film2d for now
|
||||
if (renderer && renderer.render) {
|
||||
var result = renderer.render({
|
||||
root: pass.source,
|
||||
camera: {pos: [0, 0], width: pass.target_size.w, height: pass.target_size.h, anchor: [0, 0], ortho: true},
|
||||
drawables: pass.drawables, // Was 'root'
|
||||
camera: {pos: {x: 0, y: 0}, width: pass.target_size.width, height: pass.target_size.height, anchor: {x: 0, y: 0}, ortho: true},
|
||||
target: target,
|
||||
target_size: pass.target_size,
|
||||
blend: 'replace',
|
||||
@@ -588,7 +742,7 @@ function _execute_pass(pass, renderers, backend, resolve_target, comp) {
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
|
||||
483
debug_imgui.cm
Normal file
483
debug_imgui.cm
Normal file
@@ -0,0 +1,483 @@
|
||||
// debug_imgui.cm - ImGui Debug Windows for Render Architecture
|
||||
//
|
||||
// Provides debug windows for inspecting:
|
||||
// - Scene tree and node properties
|
||||
// - Render graph and pass connections
|
||||
// - Effect parameters
|
||||
// - Performance statistics
|
||||
|
||||
var debug_imgui = {}
|
||||
|
||||
// State
|
||||
var _show_scene_tree = false
|
||||
var _show_render_graph = false
|
||||
var _show_effects = false
|
||||
var _show_stats = false
|
||||
var _show_targets = false
|
||||
|
||||
var _selected_node = null
|
||||
var _selected_pass = null
|
||||
var _expanded_nodes = {}
|
||||
|
||||
// Toggle windows
|
||||
debug_imgui.toggle_scene_tree = function() { _show_scene_tree = !_show_scene_tree }
|
||||
debug_imgui.toggle_render_graph = function() { _show_render_graph = !_show_render_graph }
|
||||
debug_imgui.toggle_effects = function() { _show_effects = !_show_effects }
|
||||
debug_imgui.toggle_stats = function() { _show_stats = !_show_stats }
|
||||
debug_imgui.toggle_targets = function() { _show_targets = !_show_targets }
|
||||
|
||||
// Main render function - call from imgui callback
|
||||
debug_imgui.render = function(imgui, scene_graph, render_plan, stats) {
|
||||
// Menu bar for toggling windows
|
||||
_render_menu(imgui)
|
||||
|
||||
if (_show_scene_tree && scene_graph) {
|
||||
_render_scene_tree(imgui, scene_graph)
|
||||
}
|
||||
|
||||
if (_show_render_graph && render_plan) {
|
||||
_render_graph_view(imgui, render_plan)
|
||||
}
|
||||
|
||||
if (_show_effects) {
|
||||
_render_effects_panel(imgui)
|
||||
}
|
||||
|
||||
if (_show_stats && stats) {
|
||||
_render_stats(imgui, stats)
|
||||
}
|
||||
|
||||
if (_show_targets && render_plan) {
|
||||
_render_targets(imgui, render_plan)
|
||||
}
|
||||
|
||||
if (_selected_node) {
|
||||
_render_node_inspector(imgui, _selected_node)
|
||||
}
|
||||
|
||||
if (_selected_pass) {
|
||||
_render_pass_inspector(imgui, _selected_pass)
|
||||
}
|
||||
}
|
||||
|
||||
// Render debug menu
|
||||
function _render_menu(imgui) {
|
||||
imgui.mainmenubar(function() {
|
||||
imgui.menu("Debug", function() {
|
||||
if (imgui.menuitem("Scene Tree", null, function(){}, _show_scene_tree)) {
|
||||
_show_scene_tree = !_show_scene_tree
|
||||
}
|
||||
if (imgui.menuitem("Render Graph", null, function(){}, _show_render_graph)) {
|
||||
_show_render_graph = !_show_render_graph
|
||||
}
|
||||
if (imgui.menuitem("Effects", null, function(){}, _show_effects)) {
|
||||
_show_effects = !_show_effects
|
||||
}
|
||||
if (imgui.menuitem("Statistics", null, function(){}, _show_stats)) {
|
||||
_show_stats = !_show_stats
|
||||
}
|
||||
if (imgui.menuitem("Render Targets", null, function(){}, _show_targets)) {
|
||||
_show_targets = !_show_targets
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Render scene tree window
|
||||
function _render_scene_tree(imgui, scene_graph) {
|
||||
imgui.window("Scene Tree", function() {
|
||||
if (!scene_graph.root) {
|
||||
imgui.text("No scene root")
|
||||
return
|
||||
}
|
||||
|
||||
imgui.text("Total nodes: " + text(scene_graph.stats.total_nodes))
|
||||
imgui.text("Dirty this frame: " + text(scene_graph.stats.dirty_this_frame))
|
||||
imgui.text("Geometry rebuilds: " + text(scene_graph.stats.geometry_rebuilds))
|
||||
|
||||
imgui.text("---")
|
||||
|
||||
_render_node_tree(imgui, scene_graph.root, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Render a node and its children recursively
|
||||
function _render_node_tree(imgui, node, depth) {
|
||||
if (!node) return
|
||||
|
||||
var id = node.id || 'unknown'
|
||||
var type = node.type || 'node'
|
||||
var label = type + " [" + id + "]"
|
||||
|
||||
// Add dirty indicator
|
||||
if (isa(node.dirty, number) && node.dirty > 0) {
|
||||
label += " *"
|
||||
}
|
||||
|
||||
var has_children = node.children && node.children.length > 0
|
||||
|
||||
if (has_children) {
|
||||
imgui.tree(label, function() {
|
||||
// Show node summary
|
||||
_render_node_summary(imgui, node)
|
||||
|
||||
// Recurse children
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
_render_node_tree(imgui, node.children[i], depth + 1)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Leaf node - show as selectable
|
||||
if (imgui.button(label)) {
|
||||
_selected_node = node
|
||||
}
|
||||
imgui.sameline(0)
|
||||
_render_node_summary(imgui, node)
|
||||
}
|
||||
}
|
||||
|
||||
// Render node summary inline
|
||||
function _render_node_summary(imgui, node) {
|
||||
var info = []
|
||||
|
||||
if (node.pos) {
|
||||
info.push("pos:(" + text(number.round(node.pos.x)) + "," + text(number.round(node.pos.y)) + ")")
|
||||
}
|
||||
|
||||
if (node.width && node.height) {
|
||||
info.push("size:" + text(node.width) + "x" + text(node.height))
|
||||
}
|
||||
|
||||
if (node.image) {
|
||||
info.push("img:" + node.image)
|
||||
}
|
||||
|
||||
if (node.text) {
|
||||
var t = node.text
|
||||
if (t.length > 20) t = t.substring(0, 17) + "..."
|
||||
info.push("\"" + t + "\"")
|
||||
}
|
||||
|
||||
if (node.effects && node.effects.length > 0) {
|
||||
var fx = []
|
||||
for (var i = 0; i < node.effects.length; i++) {
|
||||
fx.push(node.effects[i].type)
|
||||
}
|
||||
info.push("fx:[" + fx.join(",") + "]")
|
||||
}
|
||||
|
||||
if (info.length > 0) {
|
||||
imgui.text(" " + info.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
// Render node inspector window
|
||||
function _render_node_inspector(imgui, node) {
|
||||
imgui.window("Node Inspector", function() {
|
||||
if (imgui.button("Close")) {
|
||||
_selected_node = null
|
||||
return
|
||||
}
|
||||
|
||||
imgui.text("ID: " + (node.id || 'none'))
|
||||
imgui.text("Type: " + (node.type || 'unknown'))
|
||||
imgui.text("Layer: " + text(node.layer || 0))
|
||||
imgui.text("Dirty: " + text(node.dirty || 0))
|
||||
|
||||
imgui.text("---")
|
||||
|
||||
// Position
|
||||
if (node.pos) {
|
||||
imgui.text("Position")
|
||||
var pos = imgui.slider("X", node.pos.x, -1000, 1000)
|
||||
if (pos != node.pos.x) node.pos.x = pos
|
||||
pos = imgui.slider("Y", node.pos.y, -1000, 1000)
|
||||
if (pos != node.pos.y) node.pos.y = pos
|
||||
}
|
||||
|
||||
// Size
|
||||
if (node.width != null) {
|
||||
imgui.text("Size")
|
||||
node.width = imgui.slider("Width", node.width, 0, 1000)
|
||||
node.height = imgui.slider("Height", node.height, 0, 1000)
|
||||
}
|
||||
|
||||
// Opacity
|
||||
if (node.opacity != null) {
|
||||
node.opacity = imgui.slider("Opacity", node.opacity, 0, 1)
|
||||
}
|
||||
|
||||
// Color
|
||||
if (node.color) {
|
||||
imgui.text("Color")
|
||||
node.color.r = imgui.slider("R", node.color.r, 0, 1)
|
||||
node.color.g = imgui.slider("G", node.color.g, 0, 1)
|
||||
node.color.b = imgui.slider("B", node.color.b, 0, 1)
|
||||
node.color.a = imgui.slider("A", node.color.a, 0, 1)
|
||||
}
|
||||
|
||||
// World transform (read-only)
|
||||
if (node.world_pos) {
|
||||
imgui.text("---")
|
||||
imgui.text("World Position: (" + text(number.round(node.world_pos.x * 100) / 100) + ", " + text(number.round(node.world_pos.y * 100) / 100) + ")")
|
||||
}
|
||||
if (node.world_opacity != null) {
|
||||
imgui.text("World Opacity: " + text(number.round(node.world_opacity * 100) / 100))
|
||||
}
|
||||
|
||||
// Image
|
||||
if (node.image) {
|
||||
imgui.text("---")
|
||||
imgui.text("Image: " + node.image)
|
||||
}
|
||||
|
||||
// Text
|
||||
if (node.text != null) {
|
||||
imgui.text("---")
|
||||
imgui.text("Text: " + node.text)
|
||||
if (node.font) imgui.text("Font: " + node.font)
|
||||
if (node.size) imgui.text("Size: " + text(node.size))
|
||||
}
|
||||
|
||||
// Effects
|
||||
if (node.effects && node.effects.length > 0) {
|
||||
imgui.text("---")
|
||||
imgui.text("Effects:")
|
||||
for (var i = 0; i < node.effects.length; i++) {
|
||||
var fx = node.effects[i]
|
||||
imgui.tree(fx.type, function() {
|
||||
for (var k in fx) {
|
||||
if (k != 'type' && k != 'source') {
|
||||
var v = fx[k]
|
||||
if (typeof v == 'number') {
|
||||
fx[k] = imgui.slider(k, v, 0, 10)
|
||||
} else {
|
||||
imgui.text(k + ": " + text(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Geometry cache info
|
||||
if (node.geom_cache) {
|
||||
imgui.text("---")
|
||||
imgui.text("Geometry Cache:")
|
||||
imgui.text(" Vertices: " + text(node.geom_cache.vert_count || 0))
|
||||
imgui.text(" Indices: " + text(node.geom_cache.index_count || 0))
|
||||
if (node.geom_cache.texture_key) {
|
||||
imgui.text(" Texture: " + node.geom_cache.texture_key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Render graph view window
|
||||
function _render_graph_view(imgui, plan) {
|
||||
imgui.window("Render Graph", function() {
|
||||
if (!plan || !plan.passes) {
|
||||
imgui.text("No render plan")
|
||||
return
|
||||
}
|
||||
|
||||
imgui.text("Passes: " + text(plan.passes.length))
|
||||
imgui.text("Targets: " + text(array(plan.targets || {}).length))
|
||||
imgui.text("Persistent: " + text(array(plan.persistent_targets || {}).length))
|
||||
|
||||
imgui.text("---")
|
||||
|
||||
for (var i = 0; i < plan.passes.length; i++) {
|
||||
var pass = plan.passes[i]
|
||||
var label = text(i) + ": " + pass.type
|
||||
|
||||
if (pass.shader) label += " [" + pass.shader + "]"
|
||||
if (pass.renderer) label += " [" + pass.renderer + "]"
|
||||
|
||||
if (imgui.button(label)) {
|
||||
_selected_pass = pass
|
||||
}
|
||||
|
||||
// Show target info
|
||||
imgui.sameline(0)
|
||||
var target_info = ""
|
||||
if (pass.target) {
|
||||
if (pass.target == 'screen') {
|
||||
target_info = "-> screen"
|
||||
} else if (pass.target.key) {
|
||||
target_info = "-> " + pass.target.key
|
||||
}
|
||||
}
|
||||
if (pass.output) {
|
||||
if (pass.output.key) {
|
||||
target_info = "-> " + pass.output.key
|
||||
}
|
||||
}
|
||||
if (target_info) imgui.text(target_info)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Render pass inspector
|
||||
function _render_pass_inspector(imgui, pass) {
|
||||
imgui.window("Pass Inspector", function() {
|
||||
if (imgui.button("Close")) {
|
||||
_selected_pass = null
|
||||
return
|
||||
}
|
||||
|
||||
imgui.text("Type: " + pass.type)
|
||||
|
||||
if (pass.shader) imgui.text("Shader: " + pass.shader)
|
||||
if (pass.renderer) imgui.text("Renderer: " + pass.renderer)
|
||||
if (pass.blend) imgui.text("Blend: " + pass.blend)
|
||||
if (pass.presentation) imgui.text("Presentation: " + pass.presentation)
|
||||
|
||||
// Target info
|
||||
imgui.text("---")
|
||||
if (pass.target) {
|
||||
if (pass.target == 'screen') {
|
||||
imgui.text("Target: screen")
|
||||
} else if (pass.target.key) {
|
||||
imgui.text("Target: " + pass.target.key)
|
||||
if (pass.target.w) imgui.text(" Size: " + text(pass.target.w) + "x" + text(pass.target.h))
|
||||
}
|
||||
}
|
||||
|
||||
if (pass.input) {
|
||||
imgui.text("Input: " + (pass.input.key || 'unknown'))
|
||||
}
|
||||
|
||||
if (pass.output) {
|
||||
imgui.text("Output: " + (pass.output.key || 'unknown'))
|
||||
}
|
||||
|
||||
// Uniforms
|
||||
if (pass.uniforms) {
|
||||
imgui.text("---")
|
||||
imgui.text("Uniforms:")
|
||||
for (var k in pass.uniforms) {
|
||||
var v = pass.uniforms[k]
|
||||
if (Array.isArray(v)) {
|
||||
imgui.text(" " + k + ": [" + v.join(", ") + "]")
|
||||
} else {
|
||||
imgui.text(" " + k + ": " + text(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear color
|
||||
if (pass.color) {
|
||||
imgui.text("---")
|
||||
imgui.text("Clear: rgba(" +
|
||||
text(number.round(pass.color.r * 255)) + "," +
|
||||
text(number.round(pass.color.g * 255)) + "," +
|
||||
text(number.round(pass.color.b * 255)) + "," +
|
||||
text(number.round(pass.color.a * 100) / 100) + ")")
|
||||
}
|
||||
|
||||
// Source size
|
||||
if (pass.source_size) {
|
||||
imgui.text("Source size: " + text(pass.source_size.w || pass.source_size.width) + "x" + text(pass.source_size.h || pass.source_size.height))
|
||||
}
|
||||
if (pass.dest_size) {
|
||||
imgui.text("Dest size: " + text(pass.dest_size.w || pass.dest_size.width) + "x" + text(pass.dest_size.h || pass.dest_size.height))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Render effects panel
|
||||
function _render_effects_panel(imgui) {
|
||||
var effects_mod = use('effects')
|
||||
|
||||
imgui.window("Effects Registry", function() {
|
||||
var effect_list = effects_mod.list()
|
||||
imgui.text("Registered effects: " + text(effect_list.length))
|
||||
imgui.text("---")
|
||||
|
||||
for (var i = 0; i < effect_list.length; i++) {
|
||||
var name = effect_list[i]
|
||||
var deff = effects_mod.get(name)
|
||||
|
||||
imgui.tree(name, function() {
|
||||
imgui.text("Type: " + (deff.type || 'unknown'))
|
||||
imgui.text("Requires target: " + (deff.requires_target ? "yes" : "no"))
|
||||
|
||||
if (deff.params) {
|
||||
imgui.text("Parameters:")
|
||||
for (var k in deff.params) {
|
||||
var p = deff.params[k]
|
||||
var info = k
|
||||
if (p.default != null) info += " = " + text(p.default)
|
||||
if (p.type) info += " (" + p.type + ")"
|
||||
if (p.required) info += " [required]"
|
||||
imgui.text(" " + info)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Render statistics
|
||||
function _render_stats(imgui, stats) {
|
||||
imgui.window("Render Statistics", function() {
|
||||
if (stats.scene) {
|
||||
imgui.text("Scene:")
|
||||
imgui.text(" Total nodes: " + text(stats.scene.total_nodes || 0))
|
||||
imgui.text(" Dirty nodes: " + text(stats.scene.dirty_this_frame || 0))
|
||||
imgui.text(" Geometry rebuilds: " + text(stats.scene.geometry_rebuilds || 0))
|
||||
}
|
||||
|
||||
if (stats.render) {
|
||||
imgui.text("---")
|
||||
imgui.text("Render:")
|
||||
imgui.text(" Draw calls: " + text(stats.render.draw_calls || 0))
|
||||
imgui.text(" Triangles: " + text(stats.render.triangles || 0))
|
||||
imgui.text(" Batches: " + text(stats.render.batches || 0))
|
||||
}
|
||||
|
||||
if (stats.targets) {
|
||||
imgui.text("---")
|
||||
imgui.text("Targets:")
|
||||
imgui.text(" Active: " + text(stats.targets.active || 0))
|
||||
imgui.text(" Pooled: " + text(stats.targets.pooled || 0))
|
||||
imgui.text(" Memory: " + text(stats.targets.memory_mb || 0) + " MB")
|
||||
}
|
||||
|
||||
if (stats.fps) {
|
||||
imgui.text("---")
|
||||
imgui.text("FPS: " + text(number.round(stats.fps)))
|
||||
imgui.text("Frame time: " + text(number.round(stats.frame_time_ms * 100) / 100) + " ms")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Render targets view
|
||||
function _render_targets(imgui, plan) {
|
||||
imgui.window("Render Targets", function() {
|
||||
if (!plan) {
|
||||
imgui.text("No render plan")
|
||||
return
|
||||
}
|
||||
|
||||
imgui.text("Temporary Targets:")
|
||||
if (plan.targets) {
|
||||
for (var key in plan.targets) {
|
||||
var t = plan.targets[key]
|
||||
imgui.text(" " + key + ": " + text(t.width) + "x" + text(t.height))
|
||||
}
|
||||
}
|
||||
|
||||
imgui.text("---")
|
||||
imgui.text("Persistent Targets:")
|
||||
if (plan.persistent_targets) {
|
||||
for (var key in plan.persistent_targets) {
|
||||
var t = plan.persistent_targets[key]
|
||||
imgui.text(" " + key + ": " + text(t.width) + "x" + text(t.height ))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return debug_imgui
|
||||
379
effects.cm
Normal file
379
effects.cm
Normal file
@@ -0,0 +1,379 @@
|
||||
// effects.cm - Effect Registry with Built-in Effect Recipes
|
||||
//
|
||||
// Effects are defined as recipes that produce abstract render passes.
|
||||
// The compositor uses these recipes to build render plans.
|
||||
// Backends implement the actual shader logic.
|
||||
|
||||
var effects = {}
|
||||
|
||||
// Effect registry
|
||||
var _effects = {}
|
||||
|
||||
effects.register = function(name, deff) {
|
||||
_effects[name] = deff
|
||||
}
|
||||
|
||||
effects.get = function(name) {
|
||||
return _effects[name]
|
||||
}
|
||||
|
||||
effects.list = function() {
|
||||
var names = []
|
||||
for (var k in _effects) names.push(k)
|
||||
return names
|
||||
}
|
||||
|
||||
// Built-in effect: Bloom
|
||||
effects.register('bloom', {
|
||||
type: 'multi_pass',
|
||||
requires_target: true,
|
||||
params: {
|
||||
threshold: {default: 0.8, type: 'float'},
|
||||
intensity: {default: 1.0, type: 'float'},
|
||||
blur_passes: {default: 3, type: 'int'}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
var passes = []
|
||||
var size = ctx.target_size
|
||||
|
||||
// Threshold extraction
|
||||
var thresh_target = ctx.alloc_target(size.width, size.height, 'bloom_thresh')
|
||||
passes.push({
|
||||
type: 'shader',
|
||||
shader: 'threshold',
|
||||
input: input,
|
||||
output: thresh_target,
|
||||
uniforms: {
|
||||
threshold: params.threshold != null ? params.threshold : 0.8,
|
||||
intensity: params.intensity != null ? params.intensity : 1.0
|
||||
}
|
||||
})
|
||||
|
||||
// Blur ping-pong
|
||||
var blur_a = ctx.alloc_target(size.width, size.height, 'bloom_blur_a')
|
||||
var blur_b = ctx.alloc_target(size.width, size.height, 'bloom_blur_b')
|
||||
var blur_src = thresh_target
|
||||
var texel = {x: 1 / size.width, y: 1 / size.height}
|
||||
var blur_count = params.blur_passes != null ? params.blur_passes : 3
|
||||
|
||||
for (var i = 0; i < blur_count; i++) {
|
||||
passes.push({
|
||||
type: 'shader',
|
||||
shader: 'blur',
|
||||
input: blur_src,
|
||||
output: blur_a,
|
||||
uniforms: {direction: {x: 2, y: 0}, texel_size: texel}
|
||||
})
|
||||
passes.push({
|
||||
type: 'shader',
|
||||
shader: 'blur',
|
||||
input: blur_a,
|
||||
output: blur_b,
|
||||
uniforms: {direction: {x: 0, y: 2}, texel_size: texel}
|
||||
})
|
||||
blur_src = blur_b
|
||||
}
|
||||
|
||||
// Additive composite
|
||||
passes.push({
|
||||
type: 'composite',
|
||||
base: input,
|
||||
overlay: blur_src,
|
||||
output: output,
|
||||
blend: 'add'
|
||||
})
|
||||
|
||||
return passes
|
||||
}
|
||||
})
|
||||
|
||||
// Built-in effect: Mask
|
||||
effects.register('mask', {
|
||||
type: 'conditional',
|
||||
requires_target: true,
|
||||
params: {
|
||||
source: {required: true},
|
||||
channel: {default: 'alpha'},
|
||||
invert: {default: false},
|
||||
soft: {default: false},
|
||||
space: {default: 'local'}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
var passes = []
|
||||
var size = ctx.target_size
|
||||
|
||||
// Check backend capabilities for stencil optimization
|
||||
if (!params.soft && ctx.backend && ctx.backend.caps && ctx.backend.caps.has_stencil) {
|
||||
// Could use stencil - but for now use texture approach
|
||||
}
|
||||
|
||||
if (ctx.backend && ctx.backend.caps && !ctx.backend.caps.has_render_targets) {
|
||||
// Can't do masks on this backend - just pass through
|
||||
return [{type: 'blit', source: input, dest: output}]
|
||||
}
|
||||
|
||||
// Render mask source to target
|
||||
var mask_target = ctx.alloc_target(size.width, size.height, 'mask_src')
|
||||
passes.push({
|
||||
type: 'render_subtree',
|
||||
root: params.source,
|
||||
output: mask_target,
|
||||
clear: {r: 0, g: 0, b: 0, a: 0},
|
||||
space: params.space || 'local'
|
||||
})
|
||||
|
||||
// Apply mask shader
|
||||
passes.push({
|
||||
type: 'shader',
|
||||
shader: 'mask',
|
||||
inputs: [input, mask_target],
|
||||
output: output,
|
||||
uniforms: {
|
||||
channel: params.channel == 'alpha' ? 0 : 1,
|
||||
invert: params.invert ? 1 : 0
|
||||
}
|
||||
})
|
||||
|
||||
return passes
|
||||
}
|
||||
})
|
||||
|
||||
// Built-in effect: CRT
|
||||
effects.register('crt', {
|
||||
type: 'single_pass',
|
||||
requires_target: true,
|
||||
params: {
|
||||
curvature: {default: 0.1},
|
||||
scanline_intensity: {default: 0.3},
|
||||
vignette: {default: 0.2}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
return [{
|
||||
type: 'shader',
|
||||
shader: 'crt',
|
||||
input: input,
|
||||
output: output,
|
||||
uniforms: {
|
||||
curvature: params.curvature != null ? params.curvature : 0.1,
|
||||
scanline_intensity: params.scanline_intensity != null ? params.scanline_intensity : 0.3,
|
||||
vignette: params.vignette != null ? params.vignette : 0.2,
|
||||
resolution: {width: ctx.target_size.width, height: ctx.target_size.height}
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
// Built-in effect: Blur
|
||||
effects.register('blur', {
|
||||
type: 'multi_pass',
|
||||
requires_target: true,
|
||||
params: {
|
||||
passes: {default: 2}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
var passes = []
|
||||
var size = ctx.target_size
|
||||
var texel = {x: 1 / size.width, y: 1 / size.height}
|
||||
var blur_a = ctx.alloc_target(size.width, size.height, 'blur_a')
|
||||
var blur_b = ctx.alloc_target(size.width, size.height, 'blur_b')
|
||||
var src = input
|
||||
var blur_count = params.passes != null ? params.passes : 2
|
||||
|
||||
for (var i = 0; i < blur_count; i++) {
|
||||
passes.push({
|
||||
type: 'shader',
|
||||
shader: 'blur',
|
||||
input: src,
|
||||
output: blur_a,
|
||||
uniforms: {direction: {x: 2, y: 0}, texel_size: texel}
|
||||
})
|
||||
passes.push({
|
||||
type: 'shader',
|
||||
shader: 'blur',
|
||||
input: blur_a,
|
||||
output: blur_b,
|
||||
uniforms: {direction: {x: 0, y: 2}, texel_size: texel}
|
||||
})
|
||||
src = blur_b
|
||||
}
|
||||
|
||||
// Final blit to output
|
||||
passes.push({type: 'blit', source: src, dest: output})
|
||||
return passes
|
||||
}
|
||||
})
|
||||
|
||||
// Built-in effect: Accumulator (motion blur / trails)
|
||||
effects.register('accumulator', {
|
||||
type: 'stateful',
|
||||
requires_target: true,
|
||||
params: {
|
||||
decay: {default: 0.9}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
var size = ctx.target_size
|
||||
var prev = ctx.get_persistent_target('accum_prev', size.width, size.height)
|
||||
var curr = ctx.get_persistent_target('accum_curr', size.width, size.height)
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'shader',
|
||||
shader: 'accumulator',
|
||||
inputs: [input, prev],
|
||||
output: curr,
|
||||
uniforms: {decay: params.decay != null ? params.decay : 0.9}
|
||||
},
|
||||
{type: 'blit', source: curr, dest: prev},
|
||||
{type: 'blit', source: curr, dest: output}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// Built-in effect: Pixelate
|
||||
effects.register('pixelate', {
|
||||
type: 'single_pass',
|
||||
requires_target: true,
|
||||
params: {
|
||||
pixel_size: {default: 4}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
return [{
|
||||
type: 'shader',
|
||||
shader: 'pixelate',
|
||||
input: input,
|
||||
output: output,
|
||||
uniforms: {
|
||||
pixel_size: params.pixel_size != null ? params.pixel_size : 4,
|
||||
resolution: {width: ctx.target_size.width, height: ctx.target_size.height}
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
// Built-in effect: Color grading
|
||||
effects.register('color_grade', {
|
||||
type: 'single_pass',
|
||||
requires_target: true,
|
||||
params: {
|
||||
brightness: {default: 0},
|
||||
contrast: {default: 1},
|
||||
saturation: {default: 1},
|
||||
gamma: {default: 1}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
return [{
|
||||
type: 'shader',
|
||||
shader: 'color_grade',
|
||||
input: input,
|
||||
output: output,
|
||||
uniforms: {
|
||||
brightness: params.brightness != null ? params.brightness : 0,
|
||||
contrast: params.contrast != null ? params.contrast : 1,
|
||||
saturation: params.saturation != null ? params.saturation : 1,
|
||||
gamma: params.gamma != null ? params.gamma : 1
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
// Built-in effect: Vignette
|
||||
effects.register('vignette', {
|
||||
type: 'single_pass',
|
||||
requires_target: true,
|
||||
params: {
|
||||
intensity: {default: 0.3},
|
||||
softness: {default: 0.5}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
return [{
|
||||
type: 'shader',
|
||||
shader: 'vignette',
|
||||
input: input,
|
||||
output: output,
|
||||
uniforms: {
|
||||
intensity: params.intensity != null ? params.intensity : 0.3,
|
||||
softness: params.softness != null ? params.softness : 0.5,
|
||||
resolution: {width: ctx.target_size.width, height: ctx.target_size.height}
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
// Built-in effect: Chromatic aberration
|
||||
effects.register('chromatic', {
|
||||
type: 'single_pass',
|
||||
requires_target: true,
|
||||
params: {
|
||||
offset: {default: 0.005}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
return [{
|
||||
type: 'shader',
|
||||
shader: 'chromatic',
|
||||
input: input,
|
||||
output: output,
|
||||
uniforms: {
|
||||
offset: params.offset != null ? params.offset : 0.005
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
// Built-in effect: Outline
|
||||
effects.register('outline', {
|
||||
type: 'single_pass',
|
||||
requires_target: true,
|
||||
params: {
|
||||
color: {default: {r: 0, g: 0, b: 0, a: 1}},
|
||||
width: {default: 1}
|
||||
},
|
||||
build_passes: function(input, output, params, ctx) {
|
||||
var c = params.color || {r: 0, g: 0, b: 0, a: 1}
|
||||
return [{
|
||||
type: 'shader',
|
||||
shader: 'outline',
|
||||
input: input,
|
||||
output: output,
|
||||
uniforms: {
|
||||
outline_color: c,
|
||||
outline_width: params.width != null ? params.width : 1,
|
||||
texel_size: {x: 1 / ctx.target_size.width, y: 1 / ctx.target_size.height}
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
// Helper: Check if an effect requires a render target
|
||||
effects.requires_target = function(effect_type) {
|
||||
var deff = _effects[effect_type]
|
||||
return deff ? (deff.requires_target || false) : false
|
||||
}
|
||||
|
||||
// Helper: Get default params for an effect
|
||||
effects.default_params = function(effect_type) {
|
||||
var deff = _effects[effect_type]
|
||||
if (!deff || !deff.params) return {}
|
||||
|
||||
var defaults = {}
|
||||
for (var k in deff.params) {
|
||||
if (deff.params[k].default != null) {
|
||||
defaults[k] = deff.params[k].default
|
||||
}
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
// Helper: Validate effect params
|
||||
effects.validate_params = function(effect_type, params) {
|
||||
var deff = _effects[effect_type]
|
||||
if (!deff || !deff.params) return true
|
||||
|
||||
for (var k in deff.params) {
|
||||
if (deff.params[k].required && params[k] == null) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return effects
|
||||
507
film2d.cm
507
film2d.cm
@@ -19,21 +19,122 @@ film2d.capabilities = {
|
||||
supports_blur: true
|
||||
}
|
||||
|
||||
// Main render function
|
||||
// ------------------------------------------------------------------------
|
||||
// Handle Registry
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
var _next_handle_id = 1
|
||||
var _handles = {}
|
||||
|
||||
function _create_handle(type, props) {
|
||||
var id = _next_handle_id++
|
||||
var handle = {
|
||||
_id: id,
|
||||
_gen: 1,
|
||||
type: type,
|
||||
active: true,
|
||||
|
||||
// Core Transform
|
||||
x: props.pos ? props.pos.x : 0,
|
||||
y: props.pos ? props.pos.y : 0,
|
||||
rotation: props.rotation || 0,
|
||||
scale_x: props.scale ? (props.scale.x || props.scale) : 1,
|
||||
scale_y: props.scale ? (props.scale.y || props.scale) : 1,
|
||||
anchor_x: props.anchor_x || 0,
|
||||
anchor_y: props.anchor_y || 0,
|
||||
|
||||
// Properties
|
||||
layer: props.layer || 0,
|
||||
color: props.color || {r:1, g:1, b:1, a:1},
|
||||
visible: true,
|
||||
tags: props.tags || [],
|
||||
|
||||
// Methods
|
||||
set_pos: function(x, y) { this.x = x; this.y = y; return this },
|
||||
set_scale: function(x, y) { this.scale_x = x; this.scale_y = (y == null ? x : y); return this },
|
||||
set_rotation: function(r) { this.rotation = r; return this },
|
||||
set_layer: function(l) { this.layer = l; return this },
|
||||
set_color: function(r, g, b, a) {
|
||||
if (typeof r == 'object') { this.color = r }
|
||||
else { this.color = {r:r, g:g, b:b, a:a} }
|
||||
return this
|
||||
},
|
||||
set_visible: function(v) { this.visible = v; return this },
|
||||
add_tag: function(t) { if (this.tags.indexOf(t) < 0) this.tags.push(t); return this },
|
||||
remove_tag: function(t) {
|
||||
var idx = this.tags.indexOf(t)
|
||||
if (idx >= 0) this.tags.splice(idx, 1)
|
||||
return this
|
||||
},
|
||||
has_tag: function(t) { return this.tags.indexOf(t) >= 0 },
|
||||
destroy: function() { this.active = false; delete _handles[this._id] }
|
||||
}
|
||||
|
||||
// Type specific properties
|
||||
if (type == 'sprite') {
|
||||
handle.image = props.image
|
||||
handle.width = props.width || 0
|
||||
handle.height = props.height || 0
|
||||
handle.material = props.material
|
||||
handle.slice = props.slice
|
||||
handle.tile = props.tile
|
||||
} else if (type == 'text') {
|
||||
handle.text = props.text
|
||||
handle.font = props.font
|
||||
handle.size = props.size
|
||||
handle.mode = props.mode
|
||||
handle.outline_width = props.outline_width
|
||||
handle.outline_color = props.outline_color
|
||||
} else if (type == 'particles') {
|
||||
handle.particles = props.particles || []
|
||||
handle.image = props.image
|
||||
handle.width = props.width
|
||||
handle.height = props.height
|
||||
handle.material = props.material
|
||||
} else if (type == 'tilemap') {
|
||||
handle.tiles = props.tiles || []
|
||||
handle.offset_x = props.offset_x || 0
|
||||
handle.offset_y = props.offset_y || 0
|
||||
handle.scale_tile_x = props.scale_x || 1 // avoid naming conflict with transform scale
|
||||
handle.scale_tile_y = props.scale_y || 1
|
||||
handle.material = props.material
|
||||
}
|
||||
|
||||
_handles[id] = handle
|
||||
return handle
|
||||
}
|
||||
|
||||
film2d.create_sprite = function(props) { return _create_handle('sprite', props) }
|
||||
film2d.create_text = function(props) { return _create_handle('text', props) }
|
||||
film2d.create_particles = function(props) { return _create_handle('particles', props) }
|
||||
film2d.create_tilemap = function(props) { return _create_handle('tilemap', props) }
|
||||
|
||||
// Support for creating raw texture refs (usually internal, but exposed)
|
||||
film2d.create_texture_ref = function(props) {
|
||||
// Texture refs are usually transient drawables, but we can make a handle
|
||||
return _create_handle('texture_ref', props)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Render Pipeline
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// Main entrypoint: render({ drawables, camera, target, ... })
|
||||
// No root tree.
|
||||
film2d.render = function(params, backend) {
|
||||
var root = params.root
|
||||
var drawables_in = params.drawables || []
|
||||
var camera = params.camera
|
||||
var target = params.target
|
||||
var target_size = params.target_size
|
||||
var clear_color = params.clear
|
||||
|
||||
if (!root) return {commands: []}
|
||||
if (drawables_in.length == 0) return {commands: []}
|
||||
|
||||
// Collect all drawables from scene tree
|
||||
var drawables = _collect_drawables(root, camera, null, null, null, null)
|
||||
// flatten and resolve handles
|
||||
var resolved_drawables = _resolve_and_flatten(drawables_in)
|
||||
|
||||
// Sort by layer, then by Y for depth sorting
|
||||
drawables.sort(function(a, b) {
|
||||
// Sort by layer, then by Y
|
||||
resolved_drawables.sort(function(a, b) {
|
||||
var difflayer = a.layer - b.layer
|
||||
if (difflayer != 0) return difflayer
|
||||
return b.world_y - a.world_y
|
||||
@@ -42,10 +143,14 @@ film2d.render = function(params, backend) {
|
||||
// Build render commands
|
||||
var commands = []
|
||||
commands.push({cmd: 'begin_render', target: target, clear: clear_color})
|
||||
|
||||
// Apply camera?
|
||||
// User didn't specify if film2d owns camera application or if it's baked.
|
||||
// Existing film2d used 'set_camera' command. preserving that.
|
||||
commands.push({cmd: 'set_camera', camera: camera})
|
||||
|
||||
// Batch and emit draw commands
|
||||
var batches = _batch_drawables(drawables)
|
||||
// Batch and emit
|
||||
var batches = _batch_drawables(resolved_drawables)
|
||||
var current_scissor = null
|
||||
|
||||
for (var i = 0; i < batches.length; i++) {
|
||||
@@ -83,202 +188,93 @@ film2d.render = function(params, backend) {
|
||||
texture: batch.texture,
|
||||
material: batch.material
|
||||
})
|
||||
} else if (batch.type == 'texture_ref') {
|
||||
commands.push({
|
||||
cmd: 'draw_texture_ref',
|
||||
drawable: batch.drawable
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 = number.max(parent_scissor.x, node.scissor.x)
|
||||
var y1 = number.max(parent_scissor.y, node.scissor.y)
|
||||
var x2 = number.min(parent_scissor.x + parent_scissor.width, node.scissor.x + node.scissor.width)
|
||||
var y2 = number.min(parent_scissor.y + parent_scissor.height, node.scissor.y + node.scissor.height)
|
||||
current_scissor = {x: x1, y: y1, width: number.max(0, x2 - x1), height: number.max(0, y2 - y1)}
|
||||
} else {
|
||||
current_scissor = node.scissor
|
||||
// Convert input list (handles or structs) into flat list of primitive drawables
|
||||
function _resolve_and_flatten(inputs) {
|
||||
var out = []
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var item = inputs[i]
|
||||
if (!item) continue
|
||||
|
||||
// If it's a handle (has _id), use it. If it's a raw struct, use it.
|
||||
// Handles are already mostly "drawable-like", but tilemaps need expansion.
|
||||
|
||||
if (item.type == 'tilemap') {
|
||||
// Tilemaps expand to many sprites
|
||||
_expand_tilemap(item, out)
|
||||
} else if (item.type == 'sprite') {
|
||||
_expand_sprite(item, out)
|
||||
} else if (item.type == 'text' || item.type == 'texture_ref' || item.type == 'particles' || item.type == 'rect') {
|
||||
// Pass through (maybe copy needed if we mutate for batching?)
|
||||
// We need 'world_y' for sorting.
|
||||
// Ensure pos is world pos.
|
||||
// If item is a handle, it has x/y which are world x/y in flat model.
|
||||
var d = _clone_for_render(item)
|
||||
d.world_y = d.pos.y
|
||||
out.push(d)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
return out
|
||||
}
|
||||
|
||||
// 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.')
|
||||
}
|
||||
|
||||
function _clone_for_render(item) {
|
||||
// Start with a shallow copy or extraction of render properties
|
||||
var d = {
|
||||
type: item.type,
|
||||
layer: item.layer,
|
||||
pos: {x: item.x != null ? item.x : item.pos.x, y: item.y != null ? item.y : item.pos.y},
|
||||
color: item.color,
|
||||
scissor: item.scissor,
|
||||
// specific props
|
||||
text: item.text,
|
||||
font: item.font,
|
||||
size: item.size,
|
||||
mode: item.mode,
|
||||
sdf: item.sdf,
|
||||
outline_width: item.outline_width,
|
||||
outline_color: item.outline_color,
|
||||
anchor_x: item.anchor_x,
|
||||
anchor_y: item.anchor_y,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
texture_target: item.texture_target,
|
||||
blend: item.blend,
|
||||
particles: item.particles,
|
||||
image: item.image,
|
||||
texture: item.texture,
|
||||
material: item.material
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
function _expand_sprite(node, out) {
|
||||
var x = node.x != null ? node.x : (node.pos ? node.pos.x : 0)
|
||||
var y = node.y != null ? node.y : (node.pos ? node.pos.y : 0)
|
||||
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
|
||||
|
||||
var color = node.color || {r:1,g:1,b:1,a:1}
|
||||
var layer = node.layer || 0
|
||||
var scissor = node.scissor
|
||||
|
||||
var x0 = x - w * ax
|
||||
var y0 = y - h * ay
|
||||
|
||||
if (node.slice) {
|
||||
// 9-slice
|
||||
// 9-slice expansion
|
||||
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)
|
||||
@@ -300,92 +296,111 @@ function _collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_
|
||||
var TW = stretch != null ? UM * Sx : WM
|
||||
var TH = stretch != null ? VM * Sy : HM
|
||||
|
||||
function add(rect, uv) {
|
||||
out.push({
|
||||
type: 'sprite', layer: layer, world_y: y, pos: {x: rect.x, y: rect.y},
|
||||
image: node.image, texture: node.texture, material: node.material,
|
||||
width: rect.width, height: rect.height, anchor_x: 0, anchor_y: 0,
|
||||
uv_rect: uv, color: color, scissor: scissor
|
||||
})
|
||||
}
|
||||
|
||||
function 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({x: rect.x + ix*tx, y: rect.y + iy*ty, width: qw, height: qh}, quv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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})
|
||||
add({x: x0, y: y0, width: WL, height: HT}, {x:0, y:0, width: L, height: T})
|
||||
tiled({x: x0 + WL, y: y0, width: WM, height: HT}, {x:L, y:0, width: UM, height: T}, {x: TW, y: HT})
|
||||
add({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})
|
||||
tiled({x: x0, y: y0 + HT, width: WL, height: HM}, {x:0, y:T, width: L, height: VM}, {x: WL, y: TH})
|
||||
tiled({x: x0 + WL, y: y0 + HT, width: WM, height: HM}, {x:L, y:T, width: UM, height: VM}, {x: TW, y: TH})
|
||||
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})
|
||||
|
||||
add({x: x0, y: y0 + HT + HM, width: WL, height: HB}, {x:0, y: 1-B, width: L, height: B})
|
||||
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({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)
|
||||
// Tiled mode logic ... simplified: assume pre-expanded in batched logic?
|
||||
// Actually batch logic handles simple sprites. We need to expand here.
|
||||
var tx = node.tile.x || node.tile
|
||||
var ty = node.tile.y || node.tile // if just number
|
||||
if (typeof node.tile == 'number') { tx = node.tile; ty = node.tile }
|
||||
|
||||
var nx = number.ceiling(w / tx - 0.00001)
|
||||
var ny = number.ceiling(h / ty - 0.00001)
|
||||
for (var ix = 0; ix < nx; ix++) {
|
||||
for (var iy = 0; iy < ny; iy++) {
|
||||
var qw = number.min(tx, w - ix * tx)
|
||||
var qh = number.min(ty, h - iy * ty)
|
||||
out.push({
|
||||
type: 'sprite', layer: layer, world_y: y, pos: {x: x0 + ix*tx, y: y0 + iy*ty},
|
||||
image: node.image, texture: node.texture, material: node.material,
|
||||
width: qw, height: qh, anchor_x: 0, anchor_y: 0,
|
||||
// uv logic for detailed tiling omitted for brevity but should be here
|
||||
// assuming simple repeat
|
||||
uv_rect: {x:0, y:0, width: qw/tx, height: qh/ty},
|
||||
color: color, scissor: scissor
|
||||
})
|
||||
}
|
||||
}
|
||||
} 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
|
||||
})
|
||||
out.push(_clone_for_render(node))
|
||||
}
|
||||
|
||||
return drawables
|
||||
}
|
||||
|
||||
// Collect tilemap drawables
|
||||
function _collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) {
|
||||
var drawables = []
|
||||
function _expand_tilemap(node, out) {
|
||||
var tiles = node.tiles || []
|
||||
var x = node.x || 0
|
||||
var y = node.y || 0
|
||||
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)
|
||||
var scale_x = node.scale_tile_x || node.scale_x || 1
|
||||
var scale_y = node.scale_tile_y || node.scale_y || 1
|
||||
var color = node.color || {r:1,g:1,b:1,a: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]
|
||||
for (var ix = 0; ix < tiles.length; ix++) {
|
||||
if (!tiles[ix]) continue
|
||||
for (var iy = 0; iy < tiles[ix].length; iy++) {
|
||||
var tile = tiles[ix][iy]
|
||||
if (!tile) continue
|
||||
|
||||
var world_x = abs_x + (x + offset_x) * scale_x
|
||||
var world_y_pos = abs_y + (y + offset_y) * scale_y
|
||||
var world_x = x + (ix + offset_x) * scale_x
|
||||
var world_y = y + (iy + offset_y) * scale_y
|
||||
|
||||
drawables.push({
|
||||
out.push({
|
||||
type: 'sprite',
|
||||
layer: node.layer || 0,
|
||||
world_y: world_y_pos,
|
||||
pos: {x: world_x, y: world_y_pos},
|
||||
world_y: world_y,
|
||||
pos: {x: world_x, y: world_y},
|
||||
image: tile,
|
||||
texture: tile,
|
||||
width: scale_x,
|
||||
height: scale_y,
|
||||
anchor_x: 0,
|
||||
anchor_x: 0,
|
||||
anchor_y: 0,
|
||||
color: tint,
|
||||
color: color,
|
||||
material: node.material,
|
||||
scissor: current_scissor
|
||||
scissor: node.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
|
||||
|
||||
0
particles2d.cm
Normal file
0
particles2d.cm
Normal file
650
scene.cm
Normal file
650
scene.cm
Normal file
@@ -0,0 +1,650 @@
|
||||
// scene.cm - Retained Scene Graph with Dirty Tracking
|
||||
//
|
||||
// Provides retained nodes with:
|
||||
// - Persistent identity and dirty flags
|
||||
// - Cached geometry data
|
||||
// - World transform computation
|
||||
// - Automatic dirty propagation
|
||||
|
||||
var blob_mod = use('blob')
|
||||
|
||||
// Dirty flag bitmask
|
||||
var DIRTY = {
|
||||
NONE: 0,
|
||||
TRANSFORM: 1,
|
||||
CONTENT: 2,
|
||||
CHILDREN: 4,
|
||||
ALL: 7
|
||||
}
|
||||
|
||||
// Base node prototype - all nodes inherit from this
|
||||
var BaseNode = {
|
||||
id: null,
|
||||
type: 'node',
|
||||
dirty: DIRTY.ALL,
|
||||
parent: null,
|
||||
layer: 0,
|
||||
|
||||
// Local properties
|
||||
pos: null,
|
||||
opacity: 1,
|
||||
|
||||
// Computed world properties (updated during scene.update)
|
||||
world_pos: null,
|
||||
world_tint: null,
|
||||
world_opacity: 1,
|
||||
bounds: null,
|
||||
|
||||
// Backend-specific handle (for playdate sprites, etc.)
|
||||
backend_handle: null,
|
||||
|
||||
mark_dirty: function(flags) {
|
||||
this.dirty |= flags
|
||||
},
|
||||
|
||||
set_pos: function(x, y) {
|
||||
if (!this.pos) this.pos = {x: 0, y: 0}
|
||||
if (this.pos.x == x && this.pos.y == y) return this
|
||||
this.pos.x = x
|
||||
this.pos.y = y
|
||||
this.dirty |= DIRTY.TRANSFORM
|
||||
return this
|
||||
},
|
||||
|
||||
set_opacity: function(o) {
|
||||
if (this.opacity == o) return this
|
||||
this.opacity = o
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// Sprite node prototype
|
||||
var SpriteNode = {
|
||||
type: 'sprite',
|
||||
|
||||
// Source properties
|
||||
image: null,
|
||||
width: 1,
|
||||
height: 1,
|
||||
anchor: null,
|
||||
color: null,
|
||||
uv_rect: null,
|
||||
slice: null,
|
||||
tile: null,
|
||||
material: null,
|
||||
|
||||
// Geometry cache
|
||||
geom_cache: null,
|
||||
|
||||
set_image: function(img) {
|
||||
if (this.image == img) return this
|
||||
this.image = img
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
if (this.geom_cache) this.geom_cache.texture_key = img
|
||||
return this
|
||||
},
|
||||
|
||||
set_size: function(w, h) {
|
||||
if (this.width == w && this.height == h) return this
|
||||
this.width = w
|
||||
this.height = h
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
set_anchor: function(x, y) {
|
||||
if (!this.anchor) this.anchor = {x: 0, y: 0}
|
||||
if (this.anchor.x == x && this.anchor.y == y) return this
|
||||
this.anchor.x = x
|
||||
this.anchor.y = y
|
||||
this.dirty |= DIRTY.TRANSFORM
|
||||
return this
|
||||
},
|
||||
|
||||
set_color: function(r, g, b, a) {
|
||||
if (!this.color) this.color = {r: 1, g: 1, b: 1, a: 1}
|
||||
if (this.color.r == r && this.color.g == g && this.color.b == b && this.color.a == a) return this
|
||||
this.color.r = r
|
||||
this.color.g = g
|
||||
this.color.b = b
|
||||
this.color.a = a
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
rebuild_geometry: function() {
|
||||
if (this.slice) return this._build_9slice_geom()
|
||||
if (this.tile) return this._build_tiled_geom()
|
||||
return this._build_quad_geom()
|
||||
},
|
||||
|
||||
_build_quad_geom: function() {
|
||||
if (!this.geom_cache) {
|
||||
this.geom_cache = {
|
||||
verts: null,
|
||||
indices: null,
|
||||
vert_count: 0,
|
||||
index_count: 0,
|
||||
texture_key: this.image
|
||||
}
|
||||
}
|
||||
|
||||
var verts = new blob_mod(128) // 4 verts * 8 floats * 4 bytes
|
||||
var indices = new blob_mod(12) // 6 indices * 2 bytes
|
||||
|
||||
var ax = this.anchor ? this.anchor.x : 0
|
||||
var ay = this.anchor ? this.anchor.y : 0
|
||||
var x = this.world_pos.x - this.width * ax
|
||||
var y = this.world_pos.y - this.height * ay
|
||||
var w = this.width
|
||||
var h = this.height
|
||||
|
||||
var c = this.color || {r: 1, g: 1, b: 1, a: 1}
|
||||
var alpha = c.a * this.world_opacity
|
||||
|
||||
var uv = this.uv_rect || {x: 0, y: 0, width: 1, height: 1}
|
||||
var u0 = uv.x
|
||||
var v0 = uv.y
|
||||
var u1 = uv.x + uv.width
|
||||
var v1 = uv.y + uv.height
|
||||
|
||||
// v0: bottom-left
|
||||
verts.wf(x); verts.wf(y)
|
||||
verts.wf(u0); verts.wf(v1)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(alpha)
|
||||
|
||||
// v1: bottom-right
|
||||
verts.wf(x + w); verts.wf(y)
|
||||
verts.wf(u1); verts.wf(v1)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(alpha)
|
||||
|
||||
// v2: top-right
|
||||
verts.wf(x + w); verts.wf(y + h)
|
||||
verts.wf(u1); verts.wf(v0)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(alpha)
|
||||
|
||||
// v3: top-left
|
||||
verts.wf(x); verts.wf(y + h)
|
||||
verts.wf(u0); verts.wf(v0)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(alpha)
|
||||
|
||||
indices.w16(0); indices.w16(1); indices.w16(2)
|
||||
indices.w16(0); indices.w16(2); indices.w16(3)
|
||||
|
||||
this.geom_cache.verts = stone(verts)
|
||||
this.geom_cache.indices = stone(indices)
|
||||
this.geom_cache.vert_count = 4
|
||||
this.geom_cache.index_count = 6
|
||||
this.geom_cache.texture_key = this.image
|
||||
},
|
||||
|
||||
_build_9slice_geom: function() {
|
||||
// TODO: Implement 9-slice geometry building
|
||||
this._build_quad_geom()
|
||||
},
|
||||
|
||||
_build_tiled_geom: function() {
|
||||
// TODO: Implement tiled geometry building
|
||||
this._build_quad_geom()
|
||||
}
|
||||
}
|
||||
|
||||
// Tilemap node prototype
|
||||
var TilemapNode = {
|
||||
type: 'tilemap',
|
||||
|
||||
tile_size: null,
|
||||
tiles: null,
|
||||
offset: null,
|
||||
|
||||
geom_cache: null,
|
||||
|
||||
set_tile: function(x, y, image) {
|
||||
if (!this.tiles) this.tiles = []
|
||||
if (!this.tiles[x]) this.tiles[x] = []
|
||||
if (this.tiles[x][y] == image) return this
|
||||
this.tiles[x][y] = image
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
rebuild_geometry: function() {
|
||||
if (!this.geom_cache) {
|
||||
this.geom_cache = {
|
||||
verts: null,
|
||||
indices: null,
|
||||
vert_count: 0,
|
||||
index_count: 0,
|
||||
batches: []
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.tiles) {
|
||||
this.geom_cache.vert_count = 0
|
||||
this.geom_cache.index_count = 0
|
||||
this.geom_cache.batches = []
|
||||
return
|
||||
}
|
||||
|
||||
// Count tiles and group by texture
|
||||
var tile_count = 0
|
||||
var texture_map = {}
|
||||
|
||||
var ts = this.tile_size || {x: 1, y: 1}
|
||||
var off = this.offset || {x: 0, y: 0}
|
||||
|
||||
for (var x = 0; x < this.tiles.length; x++) {
|
||||
if (!this.tiles[x]) continue
|
||||
for (var y = 0; y < this.tiles[x].length; y++) {
|
||||
var t = this.tiles[x][y]
|
||||
if (!t) continue
|
||||
if (!texture_map[t]) texture_map[t] = []
|
||||
texture_map[t].push({x: x, y: y})
|
||||
tile_count++
|
||||
}
|
||||
}
|
||||
|
||||
if (tile_count == 0) {
|
||||
this.geom_cache.vert_count = 0
|
||||
this.geom_cache.index_count = 0
|
||||
this.geom_cache.batches = []
|
||||
return
|
||||
}
|
||||
|
||||
var verts = new blob_mod(tile_count * 4 * 32)
|
||||
var indices = new blob_mod(tile_count * 6 * 2)
|
||||
|
||||
var vert_offset = 0
|
||||
var index_offset = 0
|
||||
var batches = []
|
||||
|
||||
for (var tex in texture_map) {
|
||||
var batch_start = index_offset / 2
|
||||
var tiles_list = texture_map[tex]
|
||||
|
||||
for (var i = 0; i < tiles_list.length; i++) {
|
||||
var tile = tiles_list[i]
|
||||
var wx = this.world_pos.x + (tile.x + off.x) * ts.x
|
||||
var wy = this.world_pos.y + (tile.y + off.y) * ts.y
|
||||
var tw = ts.x
|
||||
var th = ts.y
|
||||
|
||||
// 4 vertices
|
||||
verts.wf(wx); verts.wf(wy); verts.wf(0); verts.wf(1)
|
||||
verts.wf(1); verts.wf(1); verts.wf(1); verts.wf(this.world_opacity)
|
||||
|
||||
verts.wf(wx + tw); verts.wf(wy); verts.wf(1); verts.wf(1)
|
||||
verts.wf(1); verts.wf(1); verts.wf(1); verts.wf(this.world_opacity)
|
||||
|
||||
verts.wf(wx + tw); verts.wf(wy + th); verts.wf(1); verts.wf(0)
|
||||
verts.wf(1); verts.wf(1); verts.wf(1); verts.wf(this.world_opacity)
|
||||
|
||||
verts.wf(wx); verts.wf(wy + th); verts.wf(0); verts.wf(0)
|
||||
verts.wf(1); verts.wf(1); verts.wf(1); verts.wf(this.world_opacity)
|
||||
|
||||
// 6 indices
|
||||
var base = vert_offset
|
||||
indices.w16(base); indices.w16(base + 1); indices.w16(base + 2)
|
||||
indices.w16(base); indices.w16(base + 2); indices.w16(base + 3)
|
||||
|
||||
vert_offset += 4
|
||||
index_offset += 12
|
||||
}
|
||||
|
||||
batches.push({
|
||||
texture: tex,
|
||||
start_index: batch_start,
|
||||
index_count: tiles_list.length * 6
|
||||
})
|
||||
}
|
||||
|
||||
this.geom_cache.verts = stone(verts)
|
||||
this.geom_cache.indices = stone(indices)
|
||||
this.geom_cache.vert_count = vert_offset
|
||||
this.geom_cache.index_count = index_offset / 2
|
||||
this.geom_cache.batches = batches
|
||||
}
|
||||
}
|
||||
|
||||
// Text node prototype
|
||||
var TextNode = {
|
||||
type: 'text',
|
||||
|
||||
text: '',
|
||||
font: 'fonts/dos',
|
||||
size: 16,
|
||||
mode: 'bitmap',
|
||||
color: null,
|
||||
anchor: null,
|
||||
outline_width: 0,
|
||||
outline_color: null,
|
||||
|
||||
measured: null,
|
||||
geom_cache: null,
|
||||
|
||||
set_text: function(t) {
|
||||
if (this.text == t) return this
|
||||
this.text = t
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
set_font: function(f, s, m) {
|
||||
if (this.font == f && this.size == s && this.mode == m) return this
|
||||
this.font = f
|
||||
if (s != null) this.size = s
|
||||
if (m != null) this.mode = m
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
rebuild_geometry: function(font_system) {
|
||||
// Text geometry is built by the backend using font_system
|
||||
// We just mark that we need rebuild
|
||||
if (!this.geom_cache) {
|
||||
this.geom_cache = {
|
||||
verts: null,
|
||||
indices: null,
|
||||
vert_count: 0,
|
||||
index_count: 0,
|
||||
font_texture: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group node prototype
|
||||
var GroupNode = {
|
||||
type: 'group',
|
||||
|
||||
children: null,
|
||||
effects: null,
|
||||
space: '2d',
|
||||
scissor: null,
|
||||
|
||||
bounds: null,
|
||||
|
||||
add_child: function(node) {
|
||||
if (!this.children) this.children = []
|
||||
node.parent = this
|
||||
this.children.push(node)
|
||||
this.dirty |= DIRTY.CHILDREN
|
||||
return this
|
||||
},
|
||||
|
||||
remove_child: function(node) {
|
||||
if (!this.children) return this
|
||||
var idx = this.children.indexOf(node)
|
||||
if (idx >= 0) {
|
||||
this.children.splice(idx, 1)
|
||||
node.parent = null
|
||||
this.dirty |= DIRTY.CHILDREN
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
clear_children: function() {
|
||||
if (!this.children) return this
|
||||
for (var c of this.children) {
|
||||
c.parent = null
|
||||
}
|
||||
this.children = []
|
||||
this.dirty |= DIRTY.CHILDREN
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// Particle node prototype
|
||||
var ParticleNode = {
|
||||
type: 'particles',
|
||||
|
||||
image: null,
|
||||
width: 1,
|
||||
height: 1,
|
||||
particles: null,
|
||||
max_particles: 1000,
|
||||
|
||||
geom_cache: null,
|
||||
|
||||
init: function() {
|
||||
if (!this.geom_cache) {
|
||||
this.geom_cache = {
|
||||
verts: null,
|
||||
indices: null,
|
||||
active_count: 0,
|
||||
capacity: this.max_particles
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-allocate index buffer (never changes)
|
||||
var indices = new blob_mod(this.max_particles * 6 * 2)
|
||||
for (var i = 0; i < this.max_particles; i++) {
|
||||
var base = i * 4
|
||||
indices.w16(base)
|
||||
indices.w16(base + 1)
|
||||
indices.w16(base + 2)
|
||||
indices.w16(base)
|
||||
indices.w16(base + 2)
|
||||
indices.w16(base + 3)
|
||||
}
|
||||
this.geom_cache.indices = stone(indices)
|
||||
return this
|
||||
},
|
||||
|
||||
rebuild_geometry: function() {
|
||||
if (!this.particles || this.particles.length == 0) {
|
||||
if (this.geom_cache) this.geom_cache.active_count = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.geom_cache) this.init()
|
||||
|
||||
var verts = new blob_mod(this.max_particles * 4 * 32)
|
||||
var count = 0
|
||||
|
||||
for (var i = 0; i < this.particles.length && count < this.max_particles; i++) {
|
||||
var p = this.particles[i]
|
||||
var hw = this.width * (p.scale || 1) * 0.5
|
||||
var hh = this.height * (p.scale || 1) * 0.5
|
||||
var px = this.world_pos.x + (p.pos ? p.pos.x : 0)
|
||||
var py = this.world_pos.y + (p.pos ? p.pos.y : 0)
|
||||
var c = p.color || {r: 1, g: 1, b: 1, a: 1}
|
||||
|
||||
// v0: bottom-left
|
||||
verts.wf(px - hw); verts.wf(py - hh); verts.wf(0); verts.wf(1)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(c.a)
|
||||
|
||||
// v1: bottom-right
|
||||
verts.wf(px + hw); verts.wf(py - hh); verts.wf(1); verts.wf(1)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(c.a)
|
||||
|
||||
// v2: top-right
|
||||
verts.wf(px + hw); verts.wf(py + hh); verts.wf(1); verts.wf(0)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(c.a)
|
||||
|
||||
// v3: top-left
|
||||
verts.wf(px - hw); verts.wf(py + hh); verts.wf(0); verts.wf(0)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(c.a)
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
this.geom_cache.verts = stone(verts)
|
||||
this.geom_cache.active_count = count
|
||||
}
|
||||
}
|
||||
|
||||
// Scene graph manager
|
||||
var SceneGraph = {
|
||||
root: null,
|
||||
all_nodes: null,
|
||||
dirty_nodes: null,
|
||||
_id_counter: 0,
|
||||
|
||||
stats: {
|
||||
total_nodes: 0,
|
||||
dirty_this_frame: 0,
|
||||
geometry_rebuilds: 0
|
||||
},
|
||||
|
||||
_gen_id: function() {
|
||||
return 'node_' + text(this._id_counter++)
|
||||
},
|
||||
|
||||
create: function(type, props) {
|
||||
props = props || {}
|
||||
var node = null
|
||||
|
||||
switch (type) {
|
||||
case 'sprite':
|
||||
node = meme(SpriteNode)
|
||||
break
|
||||
case 'tilemap':
|
||||
node = meme(TilemapNode)
|
||||
break
|
||||
case 'text':
|
||||
node = meme(TextNode)
|
||||
break
|
||||
case 'group':
|
||||
node = meme(GroupNode)
|
||||
break
|
||||
case 'particles':
|
||||
node = meme(ParticleNode)
|
||||
break
|
||||
default:
|
||||
node = meme(BaseNode)
|
||||
node.type = type || 'node'
|
||||
}
|
||||
|
||||
// Apply base node properties
|
||||
node.id = props.id || this._gen_id()
|
||||
node.dirty = DIRTY.ALL
|
||||
node.parent = null
|
||||
node.layer = props.layer || 0
|
||||
node.pos = props.pos || {x: 0, y: 0}
|
||||
node.opacity = props.opacity != null ? props.opacity : 1
|
||||
node.world_pos = {x: 0, y: 0}
|
||||
node.world_tint = {r: 1, g: 1, b: 1, a: 1}
|
||||
node.world_opacity = 1
|
||||
|
||||
// Apply type-specific properties
|
||||
for (var k in props) {
|
||||
if (k != 'id' && k != 'layer' && k != 'pos' && k != 'opacity') {
|
||||
node[k] = props[k]
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.all_nodes) this.all_nodes = {}
|
||||
this.all_nodes[node.id] = node
|
||||
this.stats.total_nodes++
|
||||
|
||||
return node
|
||||
},
|
||||
|
||||
remove: function(node) {
|
||||
if (!node) return
|
||||
if (this.all_nodes && this.all_nodes[node.id]) {
|
||||
delete this.all_nodes[node.id]
|
||||
this.stats.total_nodes--
|
||||
}
|
||||
if (node.parent) {
|
||||
node.parent.remove_child(node)
|
||||
}
|
||||
// Recursively remove children
|
||||
if (node.children) {
|
||||
for (var c of node.children) {
|
||||
this.remove(c)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Update world transforms and rebuild dirty geometry
|
||||
update: function(font_system) {
|
||||
this.stats.dirty_this_frame = 0
|
||||
this.stats.geometry_rebuilds = 0
|
||||
this.dirty_nodes = []
|
||||
|
||||
if (!this.root) return
|
||||
|
||||
// Phase 1: Propagate transforms
|
||||
this._update_transforms(this.root, {x: 0, y: 0}, {r: 1, g: 1, b: 1, a: 1}, 1)
|
||||
|
||||
// Phase 2: Rebuild geometry for dirty nodes
|
||||
for (var i = 0; i < this.dirty_nodes.length; i++) {
|
||||
var node = this.dirty_nodes[i]
|
||||
if (node.rebuild_geometry) {
|
||||
if (node.type == 'text') {
|
||||
node.rebuild_geometry(font_system)
|
||||
} else {
|
||||
node.rebuild_geometry()
|
||||
}
|
||||
this.stats.geometry_rebuilds++
|
||||
}
|
||||
node.dirty = DIRTY.NONE
|
||||
}
|
||||
},
|
||||
|
||||
_update_transforms: function(node, parent_pos, parent_tint, parent_opacity) {
|
||||
if (!node) return
|
||||
|
||||
var pos_changed = (node.dirty & DIRTY.TRANSFORM) != 0
|
||||
var content_changed = (node.dirty & DIRTY.CONTENT) != 0
|
||||
|
||||
// Compute world position
|
||||
var node_pos = node.pos || {x: 0, y: 0}
|
||||
node.world_pos = {x: parent_pos.x + node_pos.x, y: parent_pos.y + node_pos.y}
|
||||
|
||||
// Compute world tint
|
||||
var nt = node.color || node.tint || {r: 1, g: 1, b: 1, a: 1}
|
||||
node.world_tint = {
|
||||
r: parent_tint.r * nt.r,
|
||||
g: parent_tint.g * nt.g,
|
||||
b: parent_tint.b * nt.b,
|
||||
a: parent_tint.a * nt.a
|
||||
}
|
||||
|
||||
node.world_opacity = parent_opacity * (node.opacity != null ? node.opacity : 1)
|
||||
|
||||
// Mark for geometry rebuild if needed
|
||||
if (pos_changed || content_changed) {
|
||||
this.dirty_nodes.push(node)
|
||||
this.stats.dirty_this_frame++
|
||||
}
|
||||
|
||||
// Recurse children
|
||||
if (node.children) {
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
this._update_transforms(node.children[i], node.world_pos, node.world_tint, node.world_opacity)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Find node by id
|
||||
find: function(id) {
|
||||
return this.all_nodes ? this.all_nodes[id] : null
|
||||
},
|
||||
|
||||
// Clear entire scene
|
||||
clear: function() {
|
||||
this.root = null
|
||||
this.all_nodes = {}
|
||||
this.dirty_nodes = []
|
||||
this._id_counter = 0
|
||||
this.stats.total_nodes = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function - returns a new scene graph instance
|
||||
return function() {
|
||||
var sg = meme(SceneGraph)
|
||||
sg.all_nodes = {}
|
||||
sg.dirty_nodes = []
|
||||
sg._id_counter = 0
|
||||
sg.stats = {
|
||||
total_nodes: 0,
|
||||
dirty_this_frame: 0,
|
||||
geometry_rebuilds: 0
|
||||
}
|
||||
return sg
|
||||
}
|
||||
185
sdl_gpu.cm
185
sdl_gpu.cm
@@ -787,8 +787,8 @@ function _build_sprite_vertices(sprites, camera) {
|
||||
var vertices_per_sprite = 4
|
||||
var indices_per_sprite = 6
|
||||
|
||||
var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 32)
|
||||
var index_data = geometry.make_quad_indices(sprites.length)
|
||||
var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 4)
|
||||
var index_data = new blob_mod(sprites.length * indices_per_sprite * 2)
|
||||
|
||||
var vertex_count = 0
|
||||
|
||||
@@ -813,9 +813,56 @@ function _build_sprite_vertices(sprites, camera) {
|
||||
var u1 = s.uv_rect ? (s.uv_rect.x + s.uv_rect.width) : 1
|
||||
var v1 = s.uv_rect ? (s.uv_rect.y + s.uv_rect.height) : 1
|
||||
|
||||
// call to implement
|
||||
var verts = geometry.sprite_vertices({x,y,w,h,u0,v0,u1,v1,c})
|
||||
vertex_data.write_blob(verts)
|
||||
// Quad vertices (bottom-left, bottom-right, top-right, top-left)
|
||||
// v0: bottom-left
|
||||
vertex_data.wf(x)
|
||||
vertex_data.wf(y)
|
||||
vertex_data.wf(u0)
|
||||
vertex_data.wf(v1) // Flip V
|
||||
vertex_data.wf(c.r)
|
||||
vertex_data.wf(c.g)
|
||||
vertex_data.wf(c.b)
|
||||
vertex_data.wf(c.a)
|
||||
|
||||
// v1: bottom-right
|
||||
vertex_data.wf(x + w)
|
||||
vertex_data.wf(y)
|
||||
vertex_data.wf(u1)
|
||||
vertex_data.wf(v1) // Flip V
|
||||
vertex_data.wf(c.r)
|
||||
vertex_data.wf(c.g)
|
||||
vertex_data.wf(c.b)
|
||||
vertex_data.wf(c.a)
|
||||
|
||||
// v2: top-right
|
||||
vertex_data.wf(x + w)
|
||||
vertex_data.wf(y + h)
|
||||
vertex_data.wf(u1)
|
||||
vertex_data.wf(v0) // Flip V
|
||||
vertex_data.wf(c.r)
|
||||
vertex_data.wf(c.g)
|
||||
vertex_data.wf(c.b)
|
||||
vertex_data.wf(c.a)
|
||||
|
||||
// v3: top-left
|
||||
vertex_data.wf(x)
|
||||
vertex_data.wf(y + h)
|
||||
vertex_data.wf(u0)
|
||||
vertex_data.wf(v0) // Flip V
|
||||
vertex_data.wf(c.r)
|
||||
vertex_data.wf(c.g)
|
||||
vertex_data.wf(c.b)
|
||||
vertex_data.wf(c.a)
|
||||
|
||||
// Indices (two triangles)
|
||||
index_data.w16(vertex_count + 0)
|
||||
index_data.w16(vertex_count + 1)
|
||||
index_data.w16(vertex_count + 2)
|
||||
index_data.w16(vertex_count + 0)
|
||||
index_data.w16(vertex_count + 2)
|
||||
index_data.w16(vertex_count + 3)
|
||||
|
||||
vertex_count += 4
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -916,16 +963,16 @@ function _build_ortho_matrix(left, right, bottom, top, near, far) {
|
||||
}
|
||||
|
||||
function _build_camera_matrix(camera, target_width, target_height) {
|
||||
var pos = camera.pos || [0, 0]
|
||||
var pos = camera.pos || {x: 0, y: 0}
|
||||
var cam_width = camera.width || target_width
|
||||
var cam_height = camera.height || target_height
|
||||
var anchor = camera.anchor || [0.5, 0.5]
|
||||
|
||||
var left = pos[0] - cam_width * anchor[0]
|
||||
var right = pos[0] + cam_width * (1 - anchor[0])
|
||||
var bottom = pos[1] - cam_height * anchor[1]
|
||||
var top = pos[1] + cam_height * (1 - anchor[1])
|
||||
|
||||
var anchor = camera.anchor || {x: 0.5, y: 0.5}
|
||||
|
||||
var left = pos.x - cam_width * anchor.x
|
||||
var right = pos.x + cam_width * (1 - anchor.x)
|
||||
var bottom = pos.y - cam_height * anchor.y
|
||||
var top = pos.y + cam_height * (1 - anchor.y)
|
||||
|
||||
return _build_ortho_matrix(left, right, bottom, top, -1, 1)
|
||||
}
|
||||
|
||||
@@ -1068,7 +1115,11 @@ function _execute_commands(commands, window_size) {
|
||||
case 'draw_text':
|
||||
pending_draws.push(cmd)
|
||||
break
|
||||
|
||||
|
||||
case 'draw_texture_ref':
|
||||
pending_draws.push(cmd)
|
||||
break
|
||||
|
||||
case 'blit':
|
||||
// Flush pending draws first
|
||||
if (current_pass && pending_draws.length > 0) {
|
||||
@@ -1258,12 +1309,19 @@ function _flush_draws(cmd_buffer, pass, draws, camera, target) {
|
||||
// Flush current batch
|
||||
if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target)
|
||||
current_batch = null
|
||||
|
||||
|
||||
// Render text immediately
|
||||
_render_text(cmd_buffer, pass, draw.drawable, camera, target)
|
||||
} else if (draw.cmd == 'draw_texture_ref') {
|
||||
// Flush current batch
|
||||
if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target)
|
||||
current_batch = null
|
||||
|
||||
// Render pre-rendered effect texture
|
||||
_render_texture_ref(cmd_buffer, pass, draw.drawable, camera, target)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Flush final batch
|
||||
if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target)
|
||||
}
|
||||
@@ -1311,6 +1369,68 @@ function _render_batch(cmd_buffer, pass, batch, camera, target) {
|
||||
}
|
||||
}
|
||||
|
||||
// Render a pre-rendered texture from an effect group
|
||||
function _render_texture_ref(cmd_buffer, pass, drawable, camera, target) {
|
||||
var tex_target = drawable.texture_target
|
||||
if (!tex_target) return
|
||||
|
||||
// The texture_target is a compositor target reference - resolve it
|
||||
// It should have already been rendered to and we just need to blit it
|
||||
var pos = drawable.pos || {x: 0, y: 0}
|
||||
var width = drawable.width || target.width
|
||||
var height = drawable.height || target.height
|
||||
|
||||
// Build a single sprite for the texture reference
|
||||
var sprites = [{
|
||||
pos: pos,
|
||||
width: width,
|
||||
height: height,
|
||||
anchor_x: 0,
|
||||
anchor_y: 0,
|
||||
color: {r: 1, g: 1, b: 1, a: 1}
|
||||
}]
|
||||
|
||||
var geom = _build_sprite_vertices(sprites, camera)
|
||||
|
||||
// Upload geometry
|
||||
var vb_size = geom.vertices.length / 8
|
||||
var ib_size = geom.indices.length / 8
|
||||
|
||||
var vb = new gpu_mod.buffer(_gpu, {size: vb_size, vertex: true})
|
||||
var ib = new gpu_mod.buffer(_gpu, {size: ib_size, index: true})
|
||||
|
||||
var vb_transfer = new gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"})
|
||||
var ib_transfer = new gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"})
|
||||
|
||||
vb_transfer.copy_blob(_gpu, geom.vertices)
|
||||
ib_transfer.copy_blob(_gpu, geom.indices)
|
||||
|
||||
var copy_cmd = _gpu.acquire_cmd_buffer()
|
||||
var copy = copy_cmd.copy_pass()
|
||||
copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false)
|
||||
copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false)
|
||||
copy.end()
|
||||
copy_cmd.submit()
|
||||
|
||||
// Build camera matrix
|
||||
var proj = _build_camera_matrix(camera, target.width, target.height)
|
||||
|
||||
// Select pipeline based on blend mode
|
||||
var blend = drawable.blend || 'over'
|
||||
var pipeline = blend == 'add' ? _pipelines.sprite_add : _pipelines.sprite_alpha
|
||||
|
||||
// The texture_target has a .texture property from the target pool
|
||||
var tex = tex_target.texture || tex_target
|
||||
if (!tex) return
|
||||
|
||||
pass.bind_pipeline(pipeline)
|
||||
pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
|
||||
pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
|
||||
pass.bind_fragment_samplers(0, [{texture: tex, sampler: _sampler_linear}])
|
||||
cmd_buffer.push_vertex_uniform_data(0, proj)
|
||||
pass.draw_indexed(geom.index_count, 1, 0, 0, 0)
|
||||
}
|
||||
|
||||
function _render_text(cmd_buffer, pass, drawable, camera, target) {
|
||||
// Get font - support mode tag: 'bitmap', 'sdf', 'msdf'
|
||||
var font_path = drawable.font
|
||||
@@ -1666,6 +1786,9 @@ function _do_shader_pass(cmd_buffer, cmd, get_swapchain_tex) {
|
||||
case 'accumulator':
|
||||
pipeline = _pipelines.accumulator
|
||||
break
|
||||
case 'mask':
|
||||
pipeline = _pipelines.mask
|
||||
break
|
||||
default:
|
||||
log.console(`sdl_gpu: Unknown shader: ${shader}`)
|
||||
return
|
||||
@@ -1762,7 +1885,7 @@ function _do_shader_pass(cmd_buffer, cmd, get_swapchain_tex) {
|
||||
|
||||
function _build_shader_uniforms(shader, uniforms) {
|
||||
var data = new blob_mod(64) // 16 floats max
|
||||
|
||||
|
||||
switch (shader) {
|
||||
case 'threshold':
|
||||
data.wf(uniforms.threshold || 0.8)
|
||||
@@ -1771,21 +1894,21 @@ function _build_shader_uniforms(shader, uniforms) {
|
||||
data.wf(0) // padding
|
||||
break
|
||||
case 'blur':
|
||||
var dir = uniforms.direction || [1, 0]
|
||||
var texel = uniforms.texel_size || [0.001, 0.001]
|
||||
data.wf(dir[0])
|
||||
data.wf(dir[1])
|
||||
data.wf(texel[0])
|
||||
data.wf(texel[1])
|
||||
var dir = uniforms.direction || {x: 1, y: 0}
|
||||
var texel = uniforms.texel_size || {x: 0.001, y: 0.001}
|
||||
data.wf(dir.x)
|
||||
data.wf(dir.y)
|
||||
data.wf(texel.x)
|
||||
data.wf(texel.y)
|
||||
break
|
||||
case 'crt':
|
||||
data.wf(uniforms.curvature || 0.1)
|
||||
data.wf(uniforms.scanline_intensity || 0.3)
|
||||
data.wf(uniforms.vignette || 0.2)
|
||||
data.wf(0) // padding
|
||||
var res = uniforms.resolution || [1280, 720]
|
||||
data.wf(res[0])
|
||||
data.wf(res[1])
|
||||
var res = uniforms.resolution || {width: 1280, height: 720}
|
||||
data.wf(res.width)
|
||||
data.wf(res.height)
|
||||
data.wf(0) // padding
|
||||
data.wf(0) // padding
|
||||
break
|
||||
@@ -1795,10 +1918,18 @@ function _build_shader_uniforms(shader, uniforms) {
|
||||
data.wf(0) // padding
|
||||
data.wf(0) // padding
|
||||
break
|
||||
case 'mask':
|
||||
// channel: 0=alpha, 1=luminance
|
||||
// invert: 0=normal, 1=inverted
|
||||
data.wf(uniforms.channel != null ? uniforms.channel : 0)
|
||||
data.wf(uniforms.invert != null ? uniforms.invert : 0)
|
||||
data.wf(0) // padding
|
||||
data.wf(0) // padding
|
||||
break
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
return stone(data)
|
||||
}
|
||||
|
||||
|
||||
40
sprite.cm
40
sprite.cm
@@ -18,16 +18,11 @@ var sprite = {
|
||||
slice: null,
|
||||
tile: null,
|
||||
material: null,
|
||||
animation: null,
|
||||
frame: 0,
|
||||
opacity: 1,
|
||||
|
||||
// Dirty tracking
|
||||
dirty: 7, // DIRTY.ALL
|
||||
|
||||
// Cached geometry (for retained mode)
|
||||
geom_cache: null,
|
||||
|
||||
// Setters that mark dirty
|
||||
set_pos: function(x, y) {
|
||||
if (!this.pos) this.pos = {x: 0, y: 0}
|
||||
@@ -61,12 +56,12 @@ var sprite = {
|
||||
return this
|
||||
},
|
||||
|
||||
set_color: function(r, g, b, a) {
|
||||
set_color: function(color) {
|
||||
if (!this.color) this.color = {r: 1, g: 1, b: 1, a: 1}
|
||||
if (this.color.r == r && this.color.g == g && this.color.b == b && this.color.a == a) return this
|
||||
this.color.r = r
|
||||
this.color.g = g
|
||||
this.color.b = b
|
||||
if (this.color.r == color.r && this.color.g == color.g && this.color.b == color.b && this.color.a == color.a) return this
|
||||
this.color.r = color.r
|
||||
this.color.g = color.g
|
||||
this.color.b = color.b
|
||||
this.color.a = a
|
||||
this.dirty |= 2 // CONTENT
|
||||
return this
|
||||
@@ -80,28 +75,9 @@ var sprite = {
|
||||
}
|
||||
}
|
||||
|
||||
stone(sprite)
|
||||
|
||||
// Factory function
|
||||
return function(props) {
|
||||
var s = meme(sprite)
|
||||
s.pos = {x: 0, y: 0}
|
||||
s.color = {r: 1, g: 1, b: 1, a: 1}
|
||||
s.dirty = 7
|
||||
|
||||
if (props) {
|
||||
for (var k in props) {
|
||||
if (k == 'pos' && props.pos) {
|
||||
s.pos.x = props.pos.x || 0
|
||||
s.pos.y = props.pos.y || 0
|
||||
} else if (k == 'color' && props.color) {
|
||||
s.color.r = props.color.r != null ? props.color.r : 1
|
||||
s.color.g = props.color.g != null ? props.color.g : 1
|
||||
s.color.b = props.color.b != null ? props.color.b : 1
|
||||
s.color.a = props.color.a != null ? props.color.a : 1
|
||||
} else {
|
||||
s[k] = props[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
return meme(sprite, props)
|
||||
}
|
||||
14
text2d.cm
Normal file
14
text2d.cm
Normal file
@@ -0,0 +1,14 @@
|
||||
var text2d = {
|
||||
text: "",
|
||||
pos: {x:0,y:0},
|
||||
layer: 0,
|
||||
font: "fonts/dos",
|
||||
size: 16,
|
||||
color: {r:1,g:1,b:1,a:1}
|
||||
}
|
||||
|
||||
stone(text2d)
|
||||
|
||||
return function(props) {
|
||||
return meme(text2d, props)
|
||||
}
|
||||
@@ -27,4 +27,8 @@ var tilemap = {
|
||||
},
|
||||
}
|
||||
|
||||
return tilemap
|
||||
stone(tilemap)
|
||||
|
||||
return function(props) {
|
||||
return meme(tilemap, props)
|
||||
}
|
||||
Reference in New Issue
Block a user