This commit is contained in:
2026-01-01 22:01:58 -06:00
parent b61b85c3a8
commit 249b78d141
11 changed files with 2379 additions and 634 deletions

359
clay2.cm
View File

@@ -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}

View File

@@ -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
View 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
View 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
View File

@@ -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
View File

650
scene.cm Normal file
View 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
}

View File

@@ -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)
}

View File

@@ -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
View 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)
}

View File

@@ -27,4 +27,8 @@ var tilemap = {
},
}
return tilemap
stone(tilemap)
return function(props) {
return meme(tilemap, props)
}