sprite vert
This commit is contained in:
8
clay2.cm
8
clay2.cm
@@ -244,13 +244,7 @@ clay.layout = function(fn, size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function process_configs(configs) {
|
function process_configs(configs) {
|
||||||
// Merge array of configs from right to left (right overrides left)
|
return meme(base_config, ...configs)
|
||||||
// And merge with base_config
|
|
||||||
var res = meme(base_config)
|
|
||||||
for (var c of configs) {
|
|
||||||
if (c) res = meme(res, c)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function push_node(configs, contain_mode) {
|
function push_node(configs, contain_mode) {
|
||||||
|
|||||||
652
compositor.cm
652
compositor.cm
@@ -1,120 +1,108 @@
|
|||||||
// compositor.cm - High-level compositing API
|
// compositor.cm - Unified Compositor (Rewritten)
|
||||||
//
|
//
|
||||||
// The compositor orchestrates renderers (film2d, forward3d, imgui...) to produce a final image.
|
// The compositor is the SINGLE OWNER of all effects.
|
||||||
// It handles:
|
// It takes scene descriptions and produces abstract render plans.
|
||||||
// - Renderer selection per layer (film2d, forward3d, imgui, retro3d...)
|
//
|
||||||
// - Camera + viewport binding per pass
|
// Architecture:
|
||||||
// - Target ownership (composition root, fixed resolution, effect islands)
|
// Scene Tree (Retained) -> Compositor (Effect orchestration) -> Render Plan -> Backend
|
||||||
// - Presentation modes (stretch, letterbox, integer_scale, etc.)
|
|
||||||
// - Global post effects (CRT, etc.)
|
|
||||||
// - Layer stacking order
|
|
||||||
//
|
//
|
||||||
// The compositor does NOT know about sprites/tilemaps/text - that's renderer territory.
|
// The compositor does NOT know about sprites/tilemaps/text - that's renderer territory.
|
||||||
// It only knows about plates, targets, viewports, and post-processing.
|
// It only knows about plates, targets, viewports, effects, and post-processing.
|
||||||
|
|
||||||
// ========================================================================
|
var effects_mod = use('effects')
|
||||||
// PRESENTATION MODES (how child targets map into parent targets)
|
|
||||||
// ========================================================================
|
|
||||||
// "disabled" : 1:1 mapping, no scaling
|
|
||||||
// "stretch" : stretch to fill parent rect
|
|
||||||
// "letterbox" : fit by largest dimension; bars in clear color
|
|
||||||
// "overscan" : fit by smallest dimension; overflow clipped
|
|
||||||
// "integer_scale" : scale by integer multiples; nearest sampling
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// BLEND MODES (backend-agnostic)
|
|
||||||
// ========================================================================
|
|
||||||
// "replace" : overwrite destination
|
|
||||||
// "over" : alpha over (default)
|
|
||||||
// "add" : additive
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// RENDERER CAPABILITIES (queried during compilation)
|
|
||||||
// ========================================================================
|
|
||||||
// Each renderer advertises what it can do inline vs requiring an island:
|
|
||||||
// - supports_mask_stencil: can do hard masks via stencil (no target needed)
|
|
||||||
// - supports_mask_alpha: can do soft masks (needs target)
|
|
||||||
// - supports_bloom: can do bloom effect
|
|
||||||
// - supports_blur: can do blur effect
|
|
||||||
|
|
||||||
var compositor = {}
|
var compositor = {}
|
||||||
|
|
||||||
// Default renderer capabilities (conservative - requires islands for everything)
|
// Presentation modes
|
||||||
var DEFAULT_CAPS = {
|
compositor.PRESENTATION = {
|
||||||
supports_mask_stencil: true,
|
DISABLED: 'disabled',
|
||||||
supports_mask_alpha: true,
|
STRETCH: 'stretch',
|
||||||
supports_bloom: true,
|
LETTERBOX: 'letterbox',
|
||||||
supports_blur: true
|
OVERSCAN: 'overscan',
|
||||||
|
INTEGER_SCALE: 'integer_scale'
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// Blend modes
|
||||||
// COMPILATION: Turn composition tree into render plan
|
compositor.BLEND = {
|
||||||
// ========================================================================
|
REPLACE: 'replace',
|
||||||
|
OVER: 'over',
|
||||||
|
ADD: 'add'
|
||||||
|
}
|
||||||
|
|
||||||
// Compile a composition into a render plan
|
// Compile a composition tree into a render plan
|
||||||
// comp: composition description (the DSL)
|
|
||||||
// renderers: map of renderer_type -> renderer module
|
|
||||||
// backend: the render backend (sdl_gpu, etc.)
|
|
||||||
compositor.compile = function(comp, renderers, backend) {
|
compositor.compile = function(comp, renderers, backend) {
|
||||||
var ctx = {
|
var ctx = {
|
||||||
renderers: renderers,
|
renderers: renderers,
|
||||||
backend: backend,
|
backend: backend,
|
||||||
passes: [],
|
passes: [],
|
||||||
targets: {},
|
targets: {},
|
||||||
|
persistent_targets: {},
|
||||||
target_counter: 0,
|
target_counter: 0,
|
||||||
imgui_node: null
|
screen_size: null,
|
||||||
|
target_size: null,
|
||||||
|
|
||||||
|
// Target allocation
|
||||||
|
alloc_target: function(w, h, 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}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine window/screen size from backend
|
// Get screen size from backend
|
||||||
ctx.screen_size = backend.get_window_size() || {width: 1280, height: 720}
|
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.width) ctx.screen_size.width = 1280
|
||||||
if (!ctx.screen_size.height) ctx.screen_size.height = 720
|
if (!ctx.screen_size.height) ctx.screen_size.height = 720
|
||||||
|
|
||||||
// Compile the composition tree
|
// Normalize to w/h
|
||||||
var result = compile_node(comp, ctx, null, ctx.screen_size)
|
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)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
passes: ctx.passes,
|
passes: ctx.passes,
|
||||||
targets: ctx.targets,
|
targets: ctx.targets,
|
||||||
final_output: result.output
|
persistent_targets: ctx.persistent_targets,
|
||||||
|
screen_size: ctx.screen_size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile a single node in the composition tree
|
// Compile a single node
|
||||||
function compile_node(node, ctx, parent_target, parent_size) {
|
function _compile_node(node, ctx, parent_target, parent_size) {
|
||||||
if (!node) return {output: null}
|
if (!node) return {output: null}
|
||||||
|
|
||||||
var node_type = node.type
|
var node_type = node.type
|
||||||
|
|
||||||
// Determine if this node owns a target
|
|
||||||
var owns_target = false
|
var owns_target = false
|
||||||
var target = parent_target
|
var target = parent_target
|
||||||
var target_size = parent_size
|
var target_size = {w: parent_size.w || parent_size.width, h: parent_size.h || parent_size.height}
|
||||||
|
|
||||||
// Target ownership rules:
|
|
||||||
// 1. Composition root (target: "screen")
|
|
||||||
// 2. Node has explicit resolution
|
|
||||||
// 3. Node has effects that require isolation
|
|
||||||
|
|
||||||
|
// Determine target ownership
|
||||||
if (node.target == 'screen') {
|
if (node.target == 'screen') {
|
||||||
owns_target = true
|
owns_target = true
|
||||||
target = 'screen'
|
target = 'screen'
|
||||||
target_size = ctx.screen_size
|
target_size = {w: ctx.screen_size.w, h: ctx.screen_size.h}
|
||||||
} else if (node.resolution) {
|
} else if (node.resolution) {
|
||||||
owns_target = true
|
owns_target = true
|
||||||
target_size = {width: node.resolution.w || node.resolution.width || ctx.screen_size.width, height: node.resolution.h || node.resolution.height || ctx.screen_size.height}
|
target_size = {w: node.resolution.w || node.resolution.width, h: node.resolution.h || node.resolution.height}
|
||||||
target = allocate_target(ctx, target_size, node.name || 'res_target')
|
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'res_target')
|
||||||
} else if (node.effects && effects_require_island(node.effects, ctx)) {
|
} else if (node.effects && _effects_require_target(node.effects, ctx)) {
|
||||||
owns_target = true
|
owns_target = true
|
||||||
target_size = parent_size || ctx.screen_size
|
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'effect_target')
|
||||||
target = allocate_target(ctx, target_size, node.name || 'effect_island')
|
|
||||||
} else if (node.pos) {
|
|
||||||
owns_target = true
|
|
||||||
target_size = parent_size || ctx.screen_size
|
|
||||||
target = allocate_target(ctx, target_size, node.name || 'translated_target')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle clear for target owners
|
ctx.target_size = target_size
|
||||||
|
|
||||||
|
// Handle clear
|
||||||
if (owns_target && node.clear) {
|
if (owns_target && node.clear) {
|
||||||
ctx.passes.push({
|
ctx.passes.push({
|
||||||
type: 'clear',
|
type: 'clear',
|
||||||
@@ -123,21 +111,14 @@ function compile_node(node, ctx, parent_target, parent_size) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process based on node type
|
// Process by type
|
||||||
if (node_type == 'composition') {
|
if (node_type == 'composition') {
|
||||||
// Root composition - process layers
|
return _compile_composition(node, ctx, target, target_size)
|
||||||
return compile_composition(node, ctx, target, target_size)
|
|
||||||
} else if (node_type == 'group') {
|
} else if (node_type == 'group') {
|
||||||
// Group with potential effects - process layers
|
return _compile_group(node, ctx, target, target_size, parent_target, parent_size)
|
||||||
return compile_group(node, ctx, target, target_size, parent_target, parent_size)
|
} else if (_is_renderer_type(node_type)) {
|
||||||
} else if (is_renderer_type(node_type)) {
|
return _compile_renderer(node, ctx, target, target_size, parent_target, parent_size)
|
||||||
// Renderer layer (film2d, forward3d, imgui, etc.)
|
|
||||||
return compile_renderer_layer(node, ctx, target, target_size, parent_target, parent_size)
|
|
||||||
} else if (node_type == 'imgui') {
|
} else if (node_type == 'imgui') {
|
||||||
if (ctx.imgui_node) {
|
|
||||||
throw new Error("Only one imgui node is allowed in a composition")
|
|
||||||
}
|
|
||||||
ctx.imgui_node = node
|
|
||||||
ctx.passes.push({
|
ctx.passes.push({
|
||||||
type: 'imgui',
|
type: 'imgui',
|
||||||
target: target,
|
target: target,
|
||||||
@@ -151,57 +132,49 @@ function compile_node(node, ctx, parent_target, parent_size) {
|
|||||||
return {output: target}
|
return {output: target}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile a composition (root or nested)
|
// Compile composition root
|
||||||
function compile_composition(node, ctx, target, target_size) {
|
function _compile_composition(node, ctx, target, target_size) {
|
||||||
var layers = node.layers || []
|
var layers = node.layers || []
|
||||||
|
|
||||||
for (var i = 0; i < layers.length; i++) {
|
for (var i = 0; i < layers.length; i++) {
|
||||||
var layer = layers[i]
|
var layer = layers[i]
|
||||||
var is_first = (i == 0)
|
|
||||||
|
|
||||||
// Set default blend based on position
|
|
||||||
if (!layer.blend) {
|
if (!layer.blend) {
|
||||||
layer.blend = is_first ? 'replace' : 'over'
|
layer.blend = (i == 0) ? 'replace' : 'over'
|
||||||
}
|
}
|
||||||
|
_compile_node(layer, ctx, target, target_size)
|
||||||
compile_node(layer, ctx, target, target_size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {output: target}
|
return {output: target}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile a group (may have effects)
|
// Compile group with effects
|
||||||
function compile_group(node, ctx, target, target_size, parent_target, parent_size) {
|
function _compile_group(node, ctx, target, target_size, parent_target, parent_size) {
|
||||||
var layers = node.layers || []
|
var layers = node.layers || []
|
||||||
var original_target = target
|
var original_target = target
|
||||||
|
|
||||||
// Process child layers into this target
|
// Process child layers
|
||||||
for (var i = 0; i < layers.length; i++) {
|
for (var i = 0; i < layers.length; i++) {
|
||||||
var layer = layers[i]
|
var layer = layers[i]
|
||||||
var is_first = (i == 0)
|
|
||||||
|
|
||||||
if (!layer.blend) {
|
if (!layer.blend) {
|
||||||
layer.blend = is_first ? 'replace' : 'over'
|
layer.blend = (i == 0) ? 'replace' : 'over'
|
||||||
}
|
}
|
||||||
|
_compile_node(layer, ctx, target, target_size)
|
||||||
compile_node(layer, ctx, target, target_size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply effects if any
|
// Apply effects - this is where the compositor owns all effect logic
|
||||||
if (node.effects) {
|
if (node.effects && node.effects.length > 0) {
|
||||||
for (var effect of node.effects) {
|
for (var j = 0; j < node.effects.length; j++) {
|
||||||
effect._node_id = node.name || `node_${ctx.target_counter}`
|
var effect = node.effects[j]
|
||||||
target = compile_effect(effect, ctx, target, target_size)
|
target = _compile_effect(effect, ctx, target, target_size, node.name || ('group_' + ctx.target_counter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we allocated our own target (or effects changed target), composite back to parent
|
// Composite back to parent if needed
|
||||||
var needs_composite = (original_target != parent_target || target != original_target) && parent_target
|
var needs_composite = (original_target != parent_target || target != original_target) && parent_target
|
||||||
if (needs_composite) {
|
if (needs_composite) {
|
||||||
var presentation = node.presentation || 'disabled'
|
var presentation = node.presentation || 'disabled'
|
||||||
var blend = node.blend || 'over'
|
var blend = node.blend || 'over'
|
||||||
|
|
||||||
// If parent is screen, use blit_to_screen pass type
|
|
||||||
if (parent_target == 'screen') {
|
if (parent_target == 'screen') {
|
||||||
ctx.passes.push({
|
ctx.passes.push({
|
||||||
type: 'blit_to_screen',
|
type: 'blit_to_screen',
|
||||||
@@ -228,28 +201,28 @@ function compile_group(node, ctx, target, target_size, parent_target, parent_siz
|
|||||||
return {output: target}
|
return {output: target}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile a renderer layer (film2d, forward3d, etc.)
|
// Compile renderer layer (film2d, forward3d, etc.)
|
||||||
function compile_renderer_layer(node, ctx, target, target_size, parent_target, parent_size) {
|
function _compile_renderer(node, ctx, target, target_size, parent_target, parent_size) {
|
||||||
var renderer_type = node.type
|
var renderer_type = node.type
|
||||||
var renderer = ctx.renderers[renderer_type]
|
var renderer = ctx.renderers[renderer_type]
|
||||||
|
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
log.console(`compositor: Unknown renderer type: ${renderer_type}`)
|
log.console(`compositor: Unknown renderer: ${renderer_type}`)
|
||||||
return {output: target}
|
return {output: target}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this layer owns its own target
|
|
||||||
var layer_target = target
|
var layer_target = target
|
||||||
var layer_size = target_size
|
var layer_size = target_size
|
||||||
var owns_target = false
|
var owns_target = false
|
||||||
|
|
||||||
|
// Check for resolution override
|
||||||
if (node.resolution) {
|
if (node.resolution) {
|
||||||
owns_target = true
|
owns_target = true
|
||||||
layer_size = {width: node.resolution.w, height: node.resolution.h}
|
layer_size = {w: node.resolution.w, h: node.resolution.h}
|
||||||
layer_target = allocate_target(ctx, layer_size, node.name || renderer_type + '_target')
|
layer_target = ctx.alloc_target(layer_size.w, layer_size.h, node.name || renderer_type + '_target')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle clear for target owners
|
// Clear if we own target
|
||||||
if (owns_target && node.clear) {
|
if (owns_target && node.clear) {
|
||||||
ctx.passes.push({
|
ctx.passes.push({
|
||||||
type: 'clear',
|
type: 'clear',
|
||||||
@@ -270,7 +243,7 @@ function compile_renderer_layer(node, ctx, target, target_size, parent_target, p
|
|||||||
clear: owns_target ? node.clear : null
|
clear: owns_target ? node.clear : null
|
||||||
})
|
})
|
||||||
|
|
||||||
// If we have our own target, composite back to parent
|
// Composite back to parent
|
||||||
if (owns_target && parent_target) {
|
if (owns_target && parent_target) {
|
||||||
var presentation = node.presentation || 'disabled'
|
var presentation = node.presentation || 'disabled'
|
||||||
var blend = node.blend || 'over'
|
var blend = node.blend || 'over'
|
||||||
@@ -290,290 +263,128 @@ function compile_renderer_layer(node, ctx, target, target_size, parent_target, p
|
|||||||
return {output: layer_target}
|
return {output: layer_target}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile an effect
|
// Compile an effect using the effect registry
|
||||||
function compile_effect(effect, ctx, input_target, target_size) {
|
function _compile_effect(effect, ctx, input_target, target_size, node_id) {
|
||||||
var effect_type = effect.type
|
var effect_type = effect.type
|
||||||
|
var effect_def = effects_mod.get(effect_type)
|
||||||
|
|
||||||
if (effect_type == 'crt') {
|
if (!effect_def) {
|
||||||
var output = allocate_target(ctx, target_size, 'crt_output')
|
log.console(`compositor: Unknown effect: ${effect_type}`)
|
||||||
ctx.passes.push({
|
return input_target
|
||||||
type: 'shader_pass',
|
|
||||||
shader: 'crt',
|
|
||||||
input: input_target,
|
|
||||||
output: output,
|
|
||||||
uniforms: {
|
|
||||||
curvature: effect.curvature || 0.1,
|
|
||||||
scanline_intensity: effect.scanline_intensity || 0.3,
|
|
||||||
vignette: effect.vignette || 0.2,
|
|
||||||
resolution: [target_size.width, target_size.height]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effect_type == 'bloom') {
|
// Build effect context
|
||||||
return compile_bloom_effect(effect, ctx, input_target, target_size)
|
var effect_ctx = {
|
||||||
}
|
backend: ctx.backend,
|
||||||
|
target_size: {w: target_size.w, h: target_size.h},
|
||||||
if (effect_type == 'mask') {
|
alloc_target: function(w, h, hint) {
|
||||||
return compile_mask_effect(effect, ctx, input_target, target_size)
|
return ctx.alloc_target(w, h, hint)
|
||||||
}
|
},
|
||||||
|
get_persistent_target: function(key, w, h) {
|
||||||
if (effect_type == 'blur') {
|
return ctx.get_persistent_target(node_id + '_' + key, w, h)
|
||||||
return compile_blur_effect(effect, ctx, input_target, target_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (effect_type == 'accumulator') {
|
|
||||||
return compile_accumulator_effect(effect, ctx, input_target, target_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
return input_target
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile bloom effect (threshold -> blur passes -> composite)
|
|
||||||
function compile_bloom_effect(effect, ctx, input_target, target_size) {
|
|
||||||
var threshold = effect.threshold || 0.8
|
|
||||||
var intensity = effect.intensity || 1.0
|
|
||||||
var blur_passes = effect.blur_passes || 3
|
|
||||||
|
|
||||||
// Threshold pass
|
|
||||||
var threshold_output = allocate_target(ctx, target_size, 'bloom_threshold')
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'shader_pass',
|
|
||||||
shader: 'threshold',
|
|
||||||
input: input_target,
|
|
||||||
output: threshold_output,
|
|
||||||
uniforms: {threshold: threshold, intensity: intensity}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Blur passes
|
|
||||||
var blur_src = threshold_output
|
|
||||||
var texel_size = [1/target_size.width, 1/target_size.height]
|
|
||||||
|
|
||||||
for (var i = 0; i < blur_passes; i++) {
|
|
||||||
// Horizontal blur
|
|
||||||
var blur_h = allocate_target(ctx, target_size, 'bloom_blur_h_' + i)
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'shader_pass',
|
|
||||||
shader: 'blur',
|
|
||||||
input: blur_src,
|
|
||||||
output: blur_h,
|
|
||||||
uniforms: {direction: [2, 0], texel_size: texel_size}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Vertical blur
|
|
||||||
var blur_v = allocate_target(ctx, target_size, 'bloom_blur_v_' + i)
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'shader_pass',
|
|
||||||
shader: 'blur',
|
|
||||||
input: blur_h,
|
|
||||||
output: blur_v,
|
|
||||||
uniforms: {direction: [0, 2], texel_size: texel_size}
|
|
||||||
})
|
|
||||||
|
|
||||||
blur_src = blur_v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Composite bloom back onto input
|
|
||||||
var output = allocate_target(ctx, target_size, 'bloom_output')
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'composite_textures',
|
|
||||||
base: input_target,
|
|
||||||
overlay: blur_src,
|
|
||||||
output: output,
|
|
||||||
mode: 'add'
|
|
||||||
})
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile mask effect
|
|
||||||
// Optimization: use stencil for hard masks (soft: false), texture for soft masks
|
|
||||||
function compile_mask_effect(effect, ctx, input_target, target_size) {
|
|
||||||
var soft = effect.soft || false
|
|
||||||
var source = effect.source
|
|
||||||
var mode = effect.mode || 'alpha'
|
|
||||||
var space = effect.space || 'local'
|
|
||||||
var invert = effect.invert || false
|
|
||||||
|
|
||||||
// For hard masks, we could use stencil (no extra target needed)
|
|
||||||
// For now, always use texture mask approach for simplicity
|
|
||||||
// TODO: implement stencil path for hard masks
|
|
||||||
|
|
||||||
if (!soft) {
|
|
||||||
// Hard mask - could use stencil, but for now use texture
|
|
||||||
// This is where stencil optimization would go
|
|
||||||
}
|
|
||||||
|
|
||||||
// Texture mask approach (works for both soft and hard)
|
|
||||||
var mask_target = allocate_target(ctx, target_size, 'mask_source')
|
|
||||||
|
|
||||||
// Render mask source to its own target
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'render_mask_source',
|
|
||||||
source: source,
|
|
||||||
target: mask_target,
|
|
||||||
target_size: target_size,
|
|
||||||
space: space
|
|
||||||
})
|
|
||||||
|
|
||||||
// Apply mask
|
|
||||||
var output = allocate_target(ctx, target_size, 'masked_output')
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'apply_mask',
|
|
||||||
content: input_target,
|
|
||||||
mask: mask_target,
|
|
||||||
output: output,
|
|
||||||
mode: mode,
|
|
||||||
invert: invert
|
|
||||||
})
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile blur effect
|
|
||||||
function compile_blur_effect(effect, ctx, input_target, target_size) {
|
|
||||||
var passes = effect.passes || 2
|
|
||||||
var texel_size = [1/target_size.width, 1/target_size.height]
|
|
||||||
|
|
||||||
var src = input_target
|
|
||||||
|
|
||||||
for (var i = 0; i < passes; i++) {
|
|
||||||
var blur_h = allocate_target(ctx, target_size, 'blur_h_' + i)
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'shader_pass',
|
|
||||||
shader: 'blur',
|
|
||||||
input: src,
|
|
||||||
output: blur_h,
|
|
||||||
uniforms: {direction: [2, 0], texel_size: texel_size}
|
|
||||||
})
|
|
||||||
|
|
||||||
var blur_v = allocate_target(ctx, target_size, 'blur_v_' + i)
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'shader_pass',
|
|
||||||
shader: 'blur',
|
|
||||||
input: blur_h,
|
|
||||||
output: blur_v,
|
|
||||||
uniforms: {direction: [0, 2], texel_size: texel_size}
|
|
||||||
})
|
|
||||||
|
|
||||||
src = blur_v
|
|
||||||
}
|
|
||||||
|
|
||||||
return src
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile accumulator effect (motion blur)
|
|
||||||
function compile_accumulator_effect(effect, ctx, input_target, target_size) {
|
|
||||||
var decay = effect.decay != null ? effect.decay : 0.9
|
|
||||||
var node_id = effect._node_id || ctx.target_counter++
|
|
||||||
|
|
||||||
// Create persistent targets for ping-ponging
|
|
||||||
// We use stable keys based on node_id to ensure they persist across frames in the backend
|
|
||||||
var accum_prev_key = `accum_prev_${node_id}`
|
|
||||||
var accum_curr_key = `accum_curr_${node_id}`
|
|
||||||
|
|
||||||
var accum_prev = {type: 'target', key: accum_prev_key, width: target_size.width, height: target_size.height, persistent: true}
|
|
||||||
var accum_curr = {type: 'target', key: accum_curr_key, width: target_size.width, height: target_size.height, persistent: true}
|
|
||||||
|
|
||||||
// Register them in the plan so the backend knows to create them
|
|
||||||
ctx.targets[accum_prev_key] = {width: target_size.width, height: target_size.height, name: accum_prev_key, persistent: true}
|
|
||||||
ctx.targets[accum_curr_key] = {width: target_size.width, height: target_size.height, name: accum_curr_key, persistent: true}
|
|
||||||
|
|
||||||
// Accumulation pass: curr = max(input, prev * decay)
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'shader_pass',
|
|
||||||
shader: 'accumulator',
|
|
||||||
input: input_target,
|
|
||||||
extra_inputs: [accum_prev],
|
|
||||||
output: accum_curr,
|
|
||||||
uniforms: {decay: decay}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Feedback pass: copy curr back to prev for next frame
|
|
||||||
ctx.passes.push({
|
|
||||||
type: 'composite',
|
|
||||||
source: accum_curr,
|
|
||||||
dest: accum_prev,
|
|
||||||
source_size: target_size,
|
|
||||||
dest_size: target_size,
|
|
||||||
presentation: 'disabled'
|
|
||||||
})
|
|
||||||
|
|
||||||
return accum_curr
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
function allocate_target(ctx, size, name) {
|
|
||||||
name = name || 'target'
|
|
||||||
size = size || ctx.screen_size || {width: 1280, height: 720}
|
|
||||||
var w = size.width || (size.w) || 1280
|
|
||||||
var h = size.height || (size.h) || 720
|
|
||||||
|
|
||||||
var key = name + '_' + text(ctx.target_counter++)
|
|
||||||
ctx.targets[key] = {
|
|
||||||
width: w,
|
|
||||||
height: h,
|
|
||||||
name: key
|
|
||||||
}
|
|
||||||
return {type: 'target', key: key, width: w, height: h}
|
|
||||||
}
|
|
||||||
|
|
||||||
function is_renderer_type(type) {
|
|
||||||
return type == 'film2d' || type == 'forward3d' || type == 'imgui' || type == 'retro3d' || type == 'simple2d'
|
|
||||||
}
|
|
||||||
|
|
||||||
function effects_require_island(effects, ctx) {
|
|
||||||
// Check if any effect requires an offscreen target
|
|
||||||
for (var effect of effects) {
|
|
||||||
var type = effect.type
|
|
||||||
// Most effects require islands
|
|
||||||
if (type == 'bloom' || type == 'blur' || type == 'crt') return true
|
|
||||||
// Mask requires island unless we can use stencil
|
|
||||||
if (type == 'mask') {
|
|
||||||
var soft = effect.soft || false
|
|
||||||
if (soft) return true
|
|
||||||
// Hard mask could use stencil - check renderer caps
|
|
||||||
// For now, always require island
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allocate output target
|
||||||
|
var output = ctx.alloc_target(target_size.w, target_size.h, effect_type + '_out')
|
||||||
|
|
||||||
|
// Build effect passes
|
||||||
|
var effect_passes = effect_def.build_passes(input_target, output, effect, effect_ctx)
|
||||||
|
|
||||||
|
// Convert effect passes to compositor passes
|
||||||
|
for (var i = 0; i < effect_passes.length; i++) {
|
||||||
|
var ep = effect_passes[i]
|
||||||
|
ctx.passes.push(_convert_effect_pass(ep, ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert effect pass to compositor pass format
|
||||||
|
function _convert_effect_pass(ep, ctx) {
|
||||||
|
switch (ep.type) {
|
||||||
|
case 'shader':
|
||||||
|
if (!ep.input && !ep.inputs) {
|
||||||
|
ep.input = ep.inputs[0]
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'shader_pass',
|
||||||
|
shader: ep.shader,
|
||||||
|
input: ep.input || ep.inputs[0],
|
||||||
|
extra_inputs: ep.inputs ? ep.inputs.slice(1) : [],
|
||||||
|
output: ep.output,
|
||||||
|
uniforms: ep.uniforms
|
||||||
|
}
|
||||||
|
case 'composite':
|
||||||
|
return {
|
||||||
|
type: 'composite_textures',
|
||||||
|
base: ep.base,
|
||||||
|
overlay: ep.overlay,
|
||||||
|
output: ep.output,
|
||||||
|
mode: ep.blend || 'over'
|
||||||
|
}
|
||||||
|
case 'blit':
|
||||||
|
return {
|
||||||
|
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,
|
||||||
|
presentation: 'disabled'
|
||||||
|
}
|
||||||
|
case 'render_subtree':
|
||||||
|
return {
|
||||||
|
type: 'render_mask_source',
|
||||||
|
source: ep.root,
|
||||||
|
target: ep.output,
|
||||||
|
target_size: {w: ep.output.w, h: ep.output.h},
|
||||||
|
space: ep.space || 'local'
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if effects require a target
|
||||||
|
function _effects_require_target(effects, ctx) {
|
||||||
|
for (var i = 0; i < effects.length; i++) {
|
||||||
|
var effect = effects[i]
|
||||||
|
if (effects_mod.requires_target(effect.type)) return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// Check if type is a renderer
|
||||||
// PRESENTATION HELPERS
|
function _is_renderer_type(type) {
|
||||||
// ========================================================================
|
return type == 'film2d' || type == 'forward3d' || type == 'imgui' || type == 'retro3d' || type == 'simple2d'
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate destination rect for presenting source into dest
|
// Calculate presentation rect
|
||||||
compositor.calculate_presentation_rect = function(source_size, dest_size, mode) {
|
compositor.calculate_presentation_rect = function(source_size, dest_size, mode) {
|
||||||
var sw = source_size.width
|
var sw = source_size.w || source_size.width
|
||||||
var sh = source_size.height
|
var sh = source_size.h || source_size.height
|
||||||
var dw = dest_size.width
|
var dw = dest_size.w || dest_size.width
|
||||||
var dh = dest_size.height
|
var dh = dest_size.h || dest_size.height
|
||||||
|
|
||||||
if (mode == 'disabled') return {x: 0, y: 0, width: number.min(sw,dw), height: number.min(sh,dh)}
|
if (mode == 'disabled') {
|
||||||
if (mode == 'stretch') return {x: 0, y: 0, width: dw, height: dh}
|
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 src_aspect = sw / sh
|
||||||
var dst_aspect = dw / dh
|
var dst_aspect = dw / dh
|
||||||
|
|
||||||
if (mode == 'letterbox') {
|
if (mode == 'letterbox') {
|
||||||
var scale = src_aspect > dst_aspect
|
var scale = src_aspect > dst_aspect ? dw / sw : dh / sh
|
||||||
? dw / sw
|
|
||||||
: dh / sh
|
|
||||||
var w = sw * scale
|
var w = sw * scale
|
||||||
var h = sh * scale
|
var h = sh * scale
|
||||||
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode == 'overscan') {
|
if (mode == 'overscan') {
|
||||||
var scale = src_aspect > dst_aspect
|
var scale = src_aspect > dst_aspect ? dh / sh : dw / sw
|
||||||
? dh / sh
|
|
||||||
: dw / sw
|
|
||||||
var w = sw * scale
|
var w = sw * scale
|
||||||
var h = sh * scale
|
var h = sh * scale
|
||||||
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
||||||
@@ -588,22 +399,24 @@ compositor.calculate_presentation_rect = function(source_size, dest_size, mode)
|
|||||||
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: no scaling
|
|
||||||
return {x: 0, y: 0, width: sw, height: sh}
|
return {x: 0, y: 0, width: sw, height: sh}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// PLAN EXECUTION
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
// Execute a compiled render plan
|
// Execute a compiled render plan
|
||||||
compositor.execute = function(plan, renderers, backend) {
|
compositor.execute = function(plan, renderers, backend) {
|
||||||
var target_cache = {}
|
var target_cache = {}
|
||||||
|
|
||||||
// Pre-allocate all targets
|
// Pre-allocate targets
|
||||||
for (var key in plan.targets) {
|
for (var key in plan.targets) {
|
||||||
var spec = plan.targets[key]
|
var spec = plan.targets[key]
|
||||||
target_cache[key] = backend.get_or_create_target(spec.width, spec.height, key)
|
target_cache[key] = backend.get_or_create_target(spec.w, spec.h, 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve target references
|
// Resolve target references
|
||||||
@@ -616,18 +429,21 @@ compositor.execute = function(plan, renderers, backend) {
|
|||||||
// Execute passes
|
// Execute passes
|
||||||
var commands = []
|
var commands = []
|
||||||
|
|
||||||
for (var pass of plan.passes) {
|
for (var i = 0; i < plan.passes.length; i++) {
|
||||||
var pass_commands = execute_pass(pass, renderers, backend, resolve_target)
|
var pass = plan.passes[i]
|
||||||
for (var cmd of pass_commands) {
|
var pass_cmds = _execute_pass(pass, renderers, backend, resolve_target, compositor)
|
||||||
commands.push(cmd)
|
for (var j = 0; j < pass_cmds.length; j++) {
|
||||||
|
commands.push(pass_cmds[j])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {commands: commands}
|
return {commands: commands}
|
||||||
}
|
}
|
||||||
|
|
||||||
function execute_pass(pass, renderers, backend, resolve_target) {
|
// Execute a single pass
|
||||||
|
function _execute_pass(pass, renderers, backend, resolve_target, comp) {
|
||||||
var commands = []
|
var commands = []
|
||||||
|
var source
|
||||||
|
|
||||||
switch (pass.type) {
|
switch (pass.type) {
|
||||||
case 'clear':
|
case 'clear':
|
||||||
@@ -640,7 +456,7 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
|||||||
var renderer = renderers[pass.renderer]
|
var renderer = renderers[pass.renderer]
|
||||||
if (renderer && renderer.render) {
|
if (renderer && renderer.render) {
|
||||||
var target = resolve_target(pass.target)
|
var target = resolve_target(pass.target)
|
||||||
var render_result = renderer.render({
|
var result = renderer.render({
|
||||||
root: pass.root,
|
root: pass.root,
|
||||||
camera: pass.camera,
|
camera: pass.camera,
|
||||||
target: target,
|
target: target,
|
||||||
@@ -648,9 +464,9 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
|||||||
blend: pass.blend,
|
blend: pass.blend,
|
||||||
clear: pass.clear
|
clear: pass.clear
|
||||||
}, backend)
|
}, backend)
|
||||||
if (render_result && render_result.commands) {
|
if (result && result.commands) {
|
||||||
for (var cmd of render_result.commands) {
|
for (var i = 0; i < result.commands.length; i++) {
|
||||||
commands.push(cmd)
|
commands.push(result.commands[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -659,10 +475,10 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
|||||||
case 'shader_pass':
|
case 'shader_pass':
|
||||||
var input = resolve_target(pass.input)
|
var input = resolve_target(pass.input)
|
||||||
var output = resolve_target(pass.output)
|
var output = resolve_target(pass.output)
|
||||||
var extra_inputs = []
|
var extra = []
|
||||||
if (pass.extra_inputs) {
|
if (pass.extra_inputs) {
|
||||||
for (var t of pass.extra_inputs) {
|
for (var i = 0; i < pass.extra_inputs.length; i++) {
|
||||||
extra_inputs.push(resolve_target(t))
|
extra.push(resolve_target(pass.extra_inputs[i]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
commands.push({
|
commands.push({
|
||||||
@@ -670,15 +486,15 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
|||||||
shader: pass.shader,
|
shader: pass.shader,
|
||||||
input: input,
|
input: input,
|
||||||
output: output,
|
output: output,
|
||||||
extra_inputs: extra_inputs,
|
extra_inputs: extra,
|
||||||
uniforms: pass.uniforms
|
uniforms: pass.uniforms
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'composite':
|
case 'composite': {
|
||||||
var source = resolve_target(pass.source)
|
source = resolve_target(pass.source)
|
||||||
var dest = resolve_target(pass.dest)
|
var dest = resolve_target(pass.dest)
|
||||||
var rect = compositor.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
|
var rect = comp.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
|
||||||
if (pass.pos) {
|
if (pass.pos) {
|
||||||
rect.x += (pass.pos.x || 0)
|
rect.x += (pass.pos.x || 0)
|
||||||
rect.y += (pass.pos.y || 0)
|
rect.y += (pass.pos.y || 0)
|
||||||
@@ -691,11 +507,12 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
|||||||
dst_rect: rect,
|
dst_rect: rect,
|
||||||
filter: filter
|
filter: filter
|
||||||
})
|
})
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'blit_to_screen':
|
case 'blit_to_screen': {
|
||||||
var source = resolve_target(pass.source)
|
source = resolve_target(pass.source)
|
||||||
var rect = compositor.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
|
var rect = comp.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
|
||||||
if (pass.pos) {
|
if (pass.pos) {
|
||||||
rect.x += (pass.pos.x || 0)
|
rect.x += (pass.pos.x || 0)
|
||||||
rect.y += (pass.pos.y || 0)
|
rect.y += (pass.pos.y || 0)
|
||||||
@@ -708,9 +525,10 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
|||||||
dst_rect: rect,
|
dst_rect: rect,
|
||||||
filter: filter
|
filter: filter
|
||||||
})
|
})
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'composite_textures':
|
case 'composite_textures': {
|
||||||
var base = resolve_target(pass.base)
|
var base = resolve_target(pass.base)
|
||||||
var overlay = resolve_target(pass.overlay)
|
var overlay = resolve_target(pass.overlay)
|
||||||
var output = resolve_target(pass.output)
|
var output = resolve_target(pass.output)
|
||||||
@@ -721,9 +539,10 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
|||||||
output: output,
|
output: output,
|
||||||
mode: pass.mode || 'over'
|
mode: pass.mode || 'over'
|
||||||
})
|
})
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'apply_mask':
|
case 'apply_mask': {
|
||||||
var content = resolve_target(pass.content)
|
var content = resolve_target(pass.content)
|
||||||
var mask = resolve_target(pass.mask)
|
var mask = resolve_target(pass.mask)
|
||||||
var output = resolve_target(pass.output)
|
var output = resolve_target(pass.output)
|
||||||
@@ -735,37 +554,38 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
|||||||
mode: pass.mode,
|
mode: pass.mode,
|
||||||
invert: pass.invert
|
invert: pass.invert
|
||||||
})
|
})
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'imgui':
|
case 'imgui': {
|
||||||
commands.push({
|
commands.push({
|
||||||
cmd: 'imgui',
|
cmd: 'imgui',
|
||||||
draw: pass.draw,
|
draw: pass.draw,
|
||||||
rect: pass.rect,
|
rect: pass.rect,
|
||||||
target: resolve_target(pass.target)
|
target: resolve_target(pass.target)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'render_mask_source':
|
case 'render_mask_source': {
|
||||||
// This would render the mask source node
|
|
||||||
// For now, delegate to film2d renderer
|
|
||||||
var target = resolve_target(pass.target)
|
var target = resolve_target(pass.target)
|
||||||
var renderer = renderers['film2d']
|
var renderer = renderers['film2d']
|
||||||
if (renderer && renderer.render) {
|
if (renderer && renderer.render) {
|
||||||
var render_result = renderer.render({
|
var result = renderer.render({
|
||||||
root: pass.source,
|
root: pass.source,
|
||||||
camera: {pos: [0, 0], width: pass.target_size.width, height: pass.target_size.height, anchor: [0, 0], ortho: true},
|
camera: {pos: [0, 0], width: pass.target_size.w, height: pass.target_size.h, anchor: [0, 0], ortho: true},
|
||||||
target: target,
|
target: target,
|
||||||
target_size: pass.target_size,
|
target_size: pass.target_size,
|
||||||
blend: 'replace',
|
blend: 'replace',
|
||||||
clear: {r: 0, g: 0, b: 0, a: 0}
|
clear: {r: 0, g: 0, b: 0, a: 0}
|
||||||
}, backend)
|
}, backend)
|
||||||
if (render_result && render_result.commands) {
|
if (result && result.commands) {
|
||||||
for (var cmd of render_result.commands) {
|
for (var i = 0; i < result.commands.length; i++) {
|
||||||
commands.push(cmd)
|
commands.push(result.commands[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
444
film2d.cm
444
film2d.cm
@@ -1,38 +1,25 @@
|
|||||||
// film2d.cm - 2D Scene Renderer
|
// film2d.cm - 2D Scene Renderer (Rewritten)
|
||||||
//
|
//
|
||||||
// Handles scene tree traversal, sorting, batching, and draw command emission.
|
// Handles scene tree traversal, sorting, batching, and draw command emission.
|
||||||
// This is the "how to draw a plate" module - it knows about sprites, tilemaps,
|
// This is the "how to draw a 2D plate" module.
|
||||||
// text, particles, etc.
|
|
||||||
//
|
//
|
||||||
// The compositor calls this renderer with (root, camera, target, viewport, defaults)
|
// The compositor calls this renderer with (root, camera, target, target_size)
|
||||||
// and it produces draw commands.
|
// and it produces draw commands.
|
||||||
//
|
//
|
||||||
// Scene-level effects (bloom, mask, blur on groups) are handled here:
|
// This module does NOT handle effects - that's compositor territory.
|
||||||
// - Effects that can be done inline (stencil mask) are done without extra targets
|
// It only knows about sprites, tilemaps, text, particles, and rects.
|
||||||
// - Effects that need isolation (bloom, soft mask) create "islands" - render subtree
|
|
||||||
// to intermediate target, apply effect, composite back
|
|
||||||
//
|
|
||||||
// This module does NOT know about:
|
|
||||||
// - Global post effects (CRT over entire composition - that's compositor territory)
|
|
||||||
// - Other renderers (forward3d, imgui, etc.)
|
|
||||||
|
|
||||||
var film2d = {}
|
var film2d = {}
|
||||||
|
|
||||||
// Renderer capabilities - what this renderer can do inline vs requiring islands
|
// Renderer capabilities
|
||||||
film2d.capabilities = {
|
film2d.capabilities = {
|
||||||
supports_mask_stencil: true, // Hard masks via stencil (no target needed)
|
supports_mask_stencil: true,
|
||||||
supports_mask_alpha: true, // Soft masks (needs target)
|
supports_mask_alpha: true,
|
||||||
supports_bloom: true, // Bloom effect (needs target)
|
supports_bloom: true,
|
||||||
supports_blur: true // Blur effect (needs target)
|
supports_blur: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// Main render function
|
||||||
// MAIN RENDER FUNCTION
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
// Render a scene tree to a target
|
|
||||||
// params: {root, camera, target, target_size, blend, clear}
|
|
||||||
// backend: the render backend (sdl_gpu, etc.)
|
|
||||||
film2d.render = function(params, backend) {
|
film2d.render = function(params, backend) {
|
||||||
var root = params.root
|
var root = params.root
|
||||||
var camera = params.camera
|
var camera = params.camera
|
||||||
@@ -42,36 +29,31 @@ film2d.render = function(params, backend) {
|
|||||||
|
|
||||||
if (!root) return {commands: []}
|
if (!root) return {commands: []}
|
||||||
|
|
||||||
// Context for effect processing
|
// Collect all drawables from scene tree
|
||||||
var ctx = {
|
var drawables = _collect_drawables(root, camera, null, null, null, null)
|
||||||
backend: backend,
|
|
||||||
camera: camera,
|
|
||||||
target_size: target_size,
|
|
||||||
commands: [],
|
|
||||||
target_counter: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process scene tree, handling effects on groups
|
|
||||||
var drawables = process_scene_tree(root, camera, ctx)
|
|
||||||
|
|
||||||
// Sort by layer, then by Y for depth sorting
|
// Sort by layer, then by Y for depth sorting
|
||||||
|
log.console(drawables.length)
|
||||||
drawables.sort(function(a, b) {
|
drawables.sort(function(a, b) {
|
||||||
if (a.layer != b.layer) return a.layer - b.layer
|
var difflayer = a.layer - b.layer
|
||||||
|
if (difflayer != 0) return difflayer
|
||||||
return b.world_y - a.world_y
|
return b.world_y - a.world_y
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build render commands
|
// Build render commands
|
||||||
var commands = ctx.commands
|
var commands = []
|
||||||
commands.push({cmd: 'begin_render', target: target, clear: clear_color})
|
commands.push({cmd: 'begin_render', target: target, clear: clear_color})
|
||||||
commands.push({cmd: 'set_camera', camera: camera})
|
commands.push({cmd: 'set_camera', camera: camera})
|
||||||
|
|
||||||
// Batch and emit draw commands
|
// Batch and emit draw commands
|
||||||
var batches = batch_drawables(drawables)
|
var batches = _batch_drawables(drawables)
|
||||||
var current_scissor = null
|
var current_scissor = null
|
||||||
|
|
||||||
for (var batch of batches) {
|
for (var i = 0; i < batches.length; i++) {
|
||||||
// Emit scissor command if changed
|
var batch = batches[i]
|
||||||
if (!rect_equal(current_scissor, batch.scissor)) {
|
|
||||||
|
// Emit scissor if changed
|
||||||
|
if (!_rect_equal(current_scissor, batch.scissor)) {
|
||||||
commands.push({cmd: 'scissor', rect: batch.scissor})
|
commands.push({cmd: 'scissor', rect: batch.scissor})
|
||||||
current_scissor = batch.scissor
|
current_scissor = batch.scissor
|
||||||
}
|
}
|
||||||
@@ -102,15 +84,6 @@ film2d.render = function(params, backend) {
|
|||||||
texture: batch.texture,
|
texture: batch.texture,
|
||||||
material: batch.material
|
material: batch.material
|
||||||
})
|
})
|
||||||
} else if (batch.type == 'blit_target') {
|
|
||||||
// Effect island result - blit the target texture
|
|
||||||
commands.push({
|
|
||||||
cmd: 'blit',
|
|
||||||
texture: batch.drawable.target,
|
|
||||||
target: target,
|
|
||||||
dst_rect: {x: 0, y: 0, width: batch.drawable.width, height: batch.drawable.height},
|
|
||||||
filter: 'linear'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,275 +92,20 @@ film2d.render = function(params, backend) {
|
|||||||
return {target: target, commands: commands}
|
return {target: target, commands: commands}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process scene tree, handling effects on groups
|
// Collect drawables from scene tree
|
||||||
// Returns drawables for the current level, may emit commands for effect islands
|
function _collect_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos) {
|
||||||
function process_scene_tree(node, camera, ctx, parent_tint, parent_opacity, parent_scissor, parent_pos) {
|
|
||||||
if (!node) return []
|
if (!node) return []
|
||||||
|
|
||||||
// Check if this node has effects that need special handling
|
|
||||||
if (node.effects && node.effects.length > 0) {
|
|
||||||
return process_effect_group(node, camera, ctx, parent_tint, parent_opacity, parent_scissor, parent_pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// No effects - collect drawables normally
|
|
||||||
return collect_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos, ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process a group with effects - creates an "island" and applies effects sequentially
|
|
||||||
function process_effect_group(node, camera, ctx, parent_tint, parent_opacity, parent_scissor, parent_pos) {
|
|
||||||
var effects = node.effects
|
|
||||||
var backend = ctx.backend
|
|
||||||
var target_size = ctx.target_size
|
|
||||||
|
|
||||||
// 1. Render all children to an initial content target
|
|
||||||
var current_target = backend.get_or_create_target(target_size.width, target_size.height, 'effect_start_' + ctx.target_counter++)
|
|
||||||
var child_drawables = collect_children_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos, ctx)
|
|
||||||
|
|
||||||
render_drawables_to_target(child_drawables, current_target, camera, ctx)
|
|
||||||
|
|
||||||
// 2. Apply effects sequentially, each one consuming the previous target and producing a new one
|
|
||||||
for (var effect of effects) {
|
|
||||||
var effect_type = effect.type
|
|
||||||
|
|
||||||
if (effect_type == 'bloom') {
|
|
||||||
current_target = apply_bloom_effect(current_target, effect, ctx)
|
|
||||||
} else if (effect_type == 'mask') {
|
|
||||||
current_target = apply_mask_effect(current_target, effect, ctx)
|
|
||||||
} else if (effect_type == 'blur') {
|
|
||||||
current_target = apply_blur_effect(current_target, effect, ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Return a single drawable that blits the final result
|
|
||||||
return [{
|
|
||||||
type: 'blit_target',
|
|
||||||
layer: node.layer || 0,
|
|
||||||
world_y: 0,
|
|
||||||
target: current_target,
|
|
||||||
width: target_size.width,
|
|
||||||
height: target_size.height
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Render a list of drawables to a specific target
|
|
||||||
function render_drawables_to_target(drawables, target, camera, ctx) {
|
|
||||||
ctx.commands.push({cmd: 'begin_render', target: target, clear: {r: 0, g: 0, b: 0, a: 0}})
|
|
||||||
ctx.commands.push({cmd: 'set_camera', camera: camera})
|
|
||||||
|
|
||||||
var batches = batch_drawables(drawables)
|
|
||||||
for (var batch of batches) {
|
|
||||||
if (batch.type == 'sprite_batch') {
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'draw_batch',
|
|
||||||
batch_type: 'sprites',
|
|
||||||
geometry: {sprites: batch.sprites},
|
|
||||||
texture: batch.texture,
|
|
||||||
material: batch.material
|
|
||||||
})
|
|
||||||
} else if (batch.type == 'text') {
|
|
||||||
ctx.commands.push({cmd: 'draw_text', drawable: batch.drawable})
|
|
||||||
} else if (batch.type == 'particles') {
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'draw_batch',
|
|
||||||
batch_type: 'particles',
|
|
||||||
geometry: {sprites: batch.sprites},
|
|
||||||
texture: batch.texture,
|
|
||||||
material: batch.material
|
|
||||||
})
|
|
||||||
} else if (batch.type == 'blit_target') {
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'blit',
|
|
||||||
texture: batch.drawable.target,
|
|
||||||
target: target,
|
|
||||||
dst_rect: {x: 0, y: 0, width: batch.drawable.width, height: batch.drawable.height},
|
|
||||||
filter: 'linear'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.commands.push({cmd: 'end_render'})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply bloom effect to a source target, returning a new target
|
|
||||||
function apply_bloom_effect(src_target, effect, ctx) {
|
|
||||||
var backend = ctx.backend
|
|
||||||
var target_size = ctx.target_size
|
|
||||||
|
|
||||||
var threshold_target = backend.get_or_create_target(target_size.width, target_size.height, 'bloom_threshold_' + ctx.target_counter++)
|
|
||||||
var blur_target_a = backend.get_or_create_target(target_size.width, target_size.height, 'bloom_blur_a_' + ctx.target_counter++)
|
|
||||||
var blur_target_b = backend.get_or_create_target(target_size.width, target_size.height, 'bloom_blur_b_' + ctx.target_counter++)
|
|
||||||
var output_target = backend.get_or_create_target(target_size.width, target_size.height, 'bloom_output_' + ctx.target_counter++)
|
|
||||||
|
|
||||||
// Threshold pass
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'shader_pass',
|
|
||||||
shader: 'threshold',
|
|
||||||
input: src_target,
|
|
||||||
output: threshold_target,
|
|
||||||
uniforms: {threshold: effect.threshold || 0.8, intensity: effect.intensity || 1.0}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Blur passes
|
|
||||||
var blur_passes = effect.blur_passes || 3
|
|
||||||
var texel_size = [1/target_size.width, 1/target_size.height]
|
|
||||||
var blur_src = threshold_target
|
|
||||||
|
|
||||||
for (var i = 0; i < blur_passes; i++) {
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'shader_pass',
|
|
||||||
shader: 'blur',
|
|
||||||
input: blur_src,
|
|
||||||
output: blur_target_a,
|
|
||||||
uniforms: {direction: [2, 0], texel_size: texel_size}
|
|
||||||
})
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'shader_pass',
|
|
||||||
shader: 'blur',
|
|
||||||
input: blur_target_a,
|
|
||||||
output: blur_target_b,
|
|
||||||
uniforms: {direction: [0, 2], texel_size: texel_size}
|
|
||||||
})
|
|
||||||
blur_src = blur_target_b
|
|
||||||
}
|
|
||||||
|
|
||||||
// Composite bloom back onto source
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'composite_textures',
|
|
||||||
base: src_target,
|
|
||||||
overlay: blur_src,
|
|
||||||
output: output_target,
|
|
||||||
mode: 'add'
|
|
||||||
})
|
|
||||||
|
|
||||||
return output_target
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply mask effect to a source target, returning a new target
|
|
||||||
function apply_mask_effect(src_target, effect, ctx) {
|
|
||||||
var backend = ctx.backend
|
|
||||||
var target_size = ctx.target_size
|
|
||||||
var camera = ctx.camera
|
|
||||||
|
|
||||||
var mask_target = backend.get_or_create_target(target_size.width, target_size.height, 'mask_source_' + ctx.target_counter++)
|
|
||||||
var output_target = backend.get_or_create_target(target_size.width, target_size.height, 'mask_output_' + ctx.target_counter++)
|
|
||||||
|
|
||||||
// Render mask source
|
|
||||||
var mask_source = effect.source
|
|
||||||
if (mask_source) {
|
|
||||||
var mask_drawables = collect_drawables(mask_source, camera, null, null, null, null, ctx)
|
|
||||||
render_drawables_to_target(mask_drawables, mask_target, camera, ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply mask
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'apply_mask',
|
|
||||||
content_texture: src_target,
|
|
||||||
mask_texture: mask_target,
|
|
||||||
output: output_target,
|
|
||||||
mode: effect.mode || 'alpha',
|
|
||||||
invert: effect.invert || false
|
|
||||||
})
|
|
||||||
|
|
||||||
return output_target
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply blur effect to a source target, returning a new target
|
|
||||||
function apply_blur_effect(src_target, effect, ctx) {
|
|
||||||
var backend = ctx.backend
|
|
||||||
var target_size = ctx.target_size
|
|
||||||
|
|
||||||
var blur_target_a = backend.get_or_create_target(target_size.width, target_size.height, 'blur_a_' + ctx.target_counter++)
|
|
||||||
var blur_target_b = backend.get_or_create_target(target_size.width, target_size.height, 'blur_b_' + ctx.target_counter++)
|
|
||||||
|
|
||||||
// Blur passes
|
|
||||||
var blur_passes = effect.passes || 2
|
|
||||||
var texel_size = [1/target_size.width, 1/target_size.height]
|
|
||||||
var blur_src = src_target
|
|
||||||
var blur_dst = blur_target_a
|
|
||||||
|
|
||||||
for (var i = 0; i < blur_passes; i++) {
|
|
||||||
// Horizontal blur
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'shader_pass',
|
|
||||||
shader: 'blur',
|
|
||||||
input: blur_src,
|
|
||||||
output: blur_dst,
|
|
||||||
uniforms: {direction: [2, 0], texel_size: texel_size}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Swap targets
|
|
||||||
var tmp = blur_src
|
|
||||||
blur_src = blur_dst
|
|
||||||
blur_dst = (blur_dst == blur_target_a) ? blur_target_b : blur_target_a
|
|
||||||
|
|
||||||
// Vertical blur
|
|
||||||
ctx.commands.push({
|
|
||||||
cmd: 'shader_pass',
|
|
||||||
shader: 'blur',
|
|
||||||
input: blur_src,
|
|
||||||
output: blur_dst,
|
|
||||||
uniforms: {direction: [0, 2], texel_size: texel_size}
|
|
||||||
})
|
|
||||||
|
|
||||||
tmp = blur_src
|
|
||||||
blur_src = blur_dst
|
|
||||||
blur_dst = tmp
|
|
||||||
}
|
|
||||||
|
|
||||||
return blur_src
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect drawables from children only (not the node itself)
|
|
||||||
function collect_children_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos, ctx) {
|
|
||||||
var drawables = []
|
|
||||||
|
|
||||||
parent_tint = parent_tint || [1, 1, 1, 1]
|
parent_tint = parent_tint || [1, 1, 1, 1]
|
||||||
parent_opacity = parent_opacity != null ? parent_opacity : 1
|
parent_opacity = parent_opacity != null ? parent_opacity : 1
|
||||||
parent_pos = parent_pos || {x: 0, y: 0}
|
parent_pos = parent_pos || {x: 0, y: 0}
|
||||||
|
|
||||||
// Compute node 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)
|
|
||||||
|
|
||||||
// Recurse children
|
|
||||||
if (node.children) {
|
|
||||||
for (var child of node.children) {
|
|
||||||
var child_drawables = process_scene_tree(child, camera, ctx, world_tint, world_opacity, parent_scissor, current_pos)
|
|
||||||
for (var d of child_drawables) drawables.push(d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return drawables
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// SCENE TREE TRAVERSAL
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
function collect_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos, ctx) {
|
|
||||||
if (!node) return []
|
|
||||||
|
|
||||||
parent_tint = parent_tint || [1, 1, 1, 1]
|
|
||||||
parent_opacity = parent_opacity != null ? parent_opacity : 1
|
|
||||||
|
|
||||||
var drawables = []
|
var drawables = []
|
||||||
|
|
||||||
// Compute absolute position
|
// Compute absolute position
|
||||||
parent_pos = parent_pos || {x: 0, y: 0}
|
|
||||||
var node_pos = node.pos || {x: 0, y: 0}
|
var node_pos = node.pos || {x: 0, y: 0}
|
||||||
var abs_x = parent_pos.x + (node_pos.x != null ? node_pos.x : (node_pos[0] || 0))
|
var abs_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 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}
|
var current_pos = {x: abs_x, y: abs_y}
|
||||||
|
|
||||||
// Compute inherited tint/opacity
|
// Compute inherited tint/opacity
|
||||||
@@ -404,7 +122,6 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
|||||||
var current_scissor = parent_scissor
|
var current_scissor = parent_scissor
|
||||||
if (node.scissor) {
|
if (node.scissor) {
|
||||||
if (parent_scissor) {
|
if (parent_scissor) {
|
||||||
// Intersect parent and node scissor
|
|
||||||
var x1 = Math.max(parent_scissor.x, node.scissor.x)
|
var x1 = Math.max(parent_scissor.x, node.scissor.x)
|
||||||
var y1 = Math.max(parent_scissor.y, node.scissor.y)
|
var y1 = Math.max(parent_scissor.y, node.scissor.y)
|
||||||
var x2 = Math.min(parent_scissor.x + parent_scissor.width, node.scissor.x + node.scissor.width)
|
var x2 = Math.min(parent_scissor.x + parent_scissor.width, node.scissor.x + node.scissor.width)
|
||||||
@@ -417,8 +134,10 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
|||||||
|
|
||||||
// Handle different node types
|
// Handle different node types
|
||||||
if (node.type == 'sprite' || (node.image && !node.type)) {
|
if (node.type == 'sprite' || (node.image && !node.type)) {
|
||||||
var sprite_drawables = collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor)
|
var sprite_drawables = _collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor)
|
||||||
for (var d of sprite_drawables) drawables.push(d)
|
for (var i = 0; i < sprite_drawables.length; i++) {
|
||||||
|
drawables.push(sprite_drawables[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type == 'text') {
|
if (node.type == 'text') {
|
||||||
@@ -436,7 +155,7 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
|||||||
outline_color: node.outline_color,
|
outline_color: node.outline_color,
|
||||||
anchor_x: node.anchor_x,
|
anchor_x: node.anchor_x,
|
||||||
anchor_y: node.anchor_y,
|
anchor_y: node.anchor_y,
|
||||||
color: tint_to_color(world_tint, world_opacity),
|
color: _tint_to_color(world_tint, world_opacity),
|
||||||
scissor: current_scissor
|
scissor: current_scissor
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -449,14 +168,15 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
|||||||
pos: {x: abs_x, y: abs_y},
|
pos: {x: abs_x, y: abs_y},
|
||||||
width: node.width || 1,
|
width: node.width || 1,
|
||||||
height: node.height || 1,
|
height: node.height || 1,
|
||||||
color: tint_to_color(world_tint, world_opacity),
|
color: _tint_to_color(world_tint, world_opacity),
|
||||||
scissor: current_scissor
|
scissor: current_scissor
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type == 'particles' || node.particles) {
|
if (node.type == 'particles' || node.particles) {
|
||||||
var particles = node.particles || []
|
var particles = node.particles || []
|
||||||
for (var p of particles) {
|
for (var i = 0; i < particles.length; i++) {
|
||||||
|
var p = particles[i]
|
||||||
var px = p.pos ? p.pos.x : 0
|
var px = p.pos ? p.pos.x : 0
|
||||||
var py = p.pos ? p.pos.y : 0
|
var py = p.pos ? p.pos.y : 0
|
||||||
|
|
||||||
@@ -471,7 +191,7 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
|||||||
height: (node.height || 1) * (p.scale || 1),
|
height: (node.height || 1) * (p.scale || 1),
|
||||||
anchor_x: 0.5,
|
anchor_x: 0.5,
|
||||||
anchor_y: 0.5,
|
anchor_y: 0.5,
|
||||||
color: p.color || tint_to_color(world_tint, world_opacity),
|
color: p.color || _tint_to_color(world_tint, world_opacity),
|
||||||
material: node.material,
|
material: node.material,
|
||||||
scissor: current_scissor
|
scissor: current_scissor
|
||||||
})
|
})
|
||||||
@@ -479,20 +199,19 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.type == 'tilemap' || node.tiles) {
|
if (node.type == 'tilemap' || node.tiles) {
|
||||||
var tile_drawables = collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor)
|
var tile_drawables = _collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor)
|
||||||
for (var d of tile_drawables) drawables.push(d)
|
for (var i = 0; i < tile_drawables.length; i++) {
|
||||||
|
drawables.push(tile_drawables[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recurse children - use process_scene_tree if ctx available (handles effects)
|
// Recurse children
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
for (var child of node.children) {
|
for (var i = 0; i < node.children.length; i++) {
|
||||||
var child_drawables
|
var child_drawables = _collect_drawables(node.children[i], camera, world_tint, world_opacity, current_scissor, current_pos)
|
||||||
if (ctx) {
|
for (var j = 0; j < child_drawables.length; j++) {
|
||||||
child_drawables = process_scene_tree(child, camera, ctx, world_tint, world_opacity, current_scissor, current_pos)
|
drawables.push(child_drawables[j])
|
||||||
} else {
|
|
||||||
child_drawables = collect_drawables(child, camera, world_tint, world_opacity, current_scissor, current_pos, null)
|
|
||||||
}
|
}
|
||||||
for (var d of child_drawables) drawables.push(d)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,7 +219,7 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collect sprite drawables (handles slice and tile modes)
|
// Collect sprite drawables (handles slice and tile modes)
|
||||||
function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) {
|
function _collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) {
|
||||||
var drawables = []
|
var drawables = []
|
||||||
|
|
||||||
if (node.slice && node.tile) {
|
if (node.slice && node.tile) {
|
||||||
@@ -511,9 +230,8 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s
|
|||||||
var h = node.height || 1
|
var h = node.height || 1
|
||||||
var ax = node.anchor_x || 0
|
var ax = node.anchor_x || 0
|
||||||
var ay = node.anchor_y || 0
|
var ay = node.anchor_y || 0
|
||||||
var tint = tint_to_color(world_tint, world_opacity)
|
var tint = _tint_to_color(world_tint, world_opacity)
|
||||||
|
|
||||||
// Helper to add a sprite drawable
|
|
||||||
function add_sprite(rect, uv) {
|
function add_sprite(rect, uv) {
|
||||||
drawables.push({
|
drawables.push({
|
||||||
type: 'sprite',
|
type: 'sprite',
|
||||||
@@ -533,7 +251,6 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to emit tiled area
|
|
||||||
function emit_tiled(rect, uv, tile_size) {
|
function emit_tiled(rect, uv, tile_size) {
|
||||||
var tx = tile_size ? (tile_size.x || tile_size) : rect.width
|
var tx = tile_size ? (tile_size.x || tile_size) : rect.width
|
||||||
var ty = tile_size ? (tile_size.y || tile_size) : rect.height
|
var ty = tile_size ? (tile_size.y || tile_size) : rect.height
|
||||||
@@ -558,12 +275,11 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top-left of whole sprite
|
|
||||||
var x0 = abs_x - w * ax
|
var x0 = abs_x - w * ax
|
||||||
var y0 = abs_y - h * ay
|
var y0 = abs_y - h * ay
|
||||||
|
|
||||||
if (node.slice) {
|
if (node.slice) {
|
||||||
// 9-Slice logic
|
// 9-slice
|
||||||
var s = node.slice
|
var s = node.slice
|
||||||
var L = s.left != null ? s.left : (typeof s == 'number' ? s : 0)
|
var L = s.left != null ? s.left : (typeof s == 'number' ? s : 0)
|
||||||
var R = s.right != null ? s.right : (typeof s == 'number' ? s : 0)
|
var R = s.right != null ? s.right : (typeof s == 'number' ? s : 0)
|
||||||
@@ -571,51 +287,36 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s
|
|||||||
var B = s.bottom != null ? s.bottom : (typeof s == 'number' ? s : 0)
|
var B = s.bottom != null ? s.bottom : (typeof s == 'number' ? s : 0)
|
||||||
|
|
||||||
var stretch = s.stretch != null ? s.stretch : node.stretch
|
var stretch = s.stretch != null ? s.stretch : node.stretch
|
||||||
|
|
||||||
var Sx = stretch != null ? (stretch.x || stretch) : w
|
var Sx = stretch != null ? (stretch.x || stretch) : w
|
||||||
var Sy = stretch != null ? (stretch.y || stretch) : h
|
var Sy = stretch != null ? (stretch.y || stretch) : h
|
||||||
|
|
||||||
// World sizes of borders
|
|
||||||
var WL = L * Sx
|
var WL = L * Sx
|
||||||
var WR = R * Sx
|
var WR = R * Sx
|
||||||
var HT = T * Sy
|
var HT = T * Sy
|
||||||
var HB = B * Sy
|
var HB = B * Sy
|
||||||
|
|
||||||
// Middle areas
|
|
||||||
var WM = w - WL - WR
|
var WM = w - WL - WR
|
||||||
var HM = h - HT - HB
|
var HM = h - HT - HB
|
||||||
|
|
||||||
// UV mid dimensions
|
|
||||||
var UM = 1 - L - R
|
var UM = 1 - L - R
|
||||||
var VM = 1 - T - B
|
var VM = 1 - T - B
|
||||||
|
|
||||||
// Natural tile sizes for middle parts
|
|
||||||
var TW = stretch != null ? UM * Sx : WM
|
var TW = stretch != null ? UM * Sx : WM
|
||||||
var TH = stretch != null ? VM * Sy : HM
|
var TH = stretch != null ? VM * Sy : HM
|
||||||
|
|
||||||
// TL
|
// TL, TM, TR
|
||||||
add_sprite({x: x0, y: y0, width: WL, height: HT}, {x:0, y:0, width: L, height: T})
|
add_sprite({x: x0, y: y0, width: WL, height: HT}, {x:0, y:0, width: L, height: T})
|
||||||
// TM
|
|
||||||
emit_tiled({x: x0 + WL, y: y0, width: WM, height: HT}, {x:L, y:0, width: UM, height: T}, {x: TW, y: HT})
|
emit_tiled({x: x0 + WL, y: y0, width: WM, height: HT}, {x:L, y:0, width: UM, height: T}, {x: TW, y: HT})
|
||||||
// TR
|
|
||||||
add_sprite({x: x0 + WL + WM, y: y0, width: WR, height: HT}, {x: 1-R, y:0, width: R, height: T})
|
add_sprite({x: x0 + WL + WM, y: y0, width: WR, height: HT}, {x: 1-R, y:0, width: R, height: T})
|
||||||
|
|
||||||
// ML
|
// 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, y: y0 + HT, width: WL, height: HM}, {x:0, y:T, width: L, height: VM}, {x: WL, y: TH})
|
||||||
// MM
|
|
||||||
emit_tiled({x: x0 + WL, y: y0 + HT, width: WM, height: HM}, {x:L, y:T, width: UM, height: VM}, {x: TW, y: TH})
|
emit_tiled({x: x0 + WL, y: y0 + HT, width: WM, height: HM}, {x:L, y:T, width: UM, height: VM}, {x: TW, y: TH})
|
||||||
// MR
|
|
||||||
emit_tiled({x: x0 + WL + WM, y: y0 + HT, width: WR, height: HM}, {x: 1-R, y:T, width: R, height: VM}, {x: WR, y: TH})
|
emit_tiled({x: x0 + WL + WM, y: y0 + HT, width: WR, height: HM}, {x: 1-R, y:T, width: R, height: VM}, {x: WR, y: TH})
|
||||||
|
|
||||||
// BL
|
// BL, BM, BR
|
||||||
add_sprite({x: x0, y: y0 + HT + HM, width: WL, height: HB}, {x:0, y: 1-B, width: L, height: B})
|
add_sprite({x: x0, y: y0 + HT + HM, width: WL, height: HB}, {x:0, y: 1-B, width: L, height: B})
|
||||||
// BM
|
|
||||||
emit_tiled({x: x0 + WL, y: y0 + HT + HM, width: WM, height: HB}, {x:L, y: 1-B, width: UM, height: B}, {x: TW, y: HB})
|
emit_tiled({x: x0 + WL, y: y0 + HT + HM, width: WM, height: HB}, {x:L, y: 1-B, width: UM, height: B}, {x: TW, y: HB})
|
||||||
// BR
|
|
||||||
add_sprite({x: x0 + WL + WM, y: y0 + HT + HM, width: WR, height: HB}, {x: 1-R, y: 1-B, width: R, height: B})
|
add_sprite({x: x0 + WL + WM, y: y0 + HT + HM, width: WR, height: HB}, {x: 1-R, y: 1-B, width: R, height: B})
|
||||||
|
|
||||||
} else if (node.tile) {
|
} else if (node.tile) {
|
||||||
// Full sprite tiling
|
|
||||||
emit_tiled({x: x0, y: y0, width: w, height: h}, {x:0, y:0, width: 1, height: 1}, node.tile)
|
emit_tiled({x: x0, y: y0, width: w, height: h}, {x:0, y:0, width: 1, height: 1}, node.tile)
|
||||||
} else {
|
} else {
|
||||||
// Normal sprite
|
// Normal sprite
|
||||||
@@ -640,14 +341,14 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collect tilemap drawables
|
// Collect tilemap drawables
|
||||||
function collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) {
|
function _collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) {
|
||||||
var drawables = []
|
var drawables = []
|
||||||
var tiles = node.tiles || []
|
var tiles = node.tiles || []
|
||||||
var offset_x = node.offset_x || 0
|
var offset_x = node.offset_x || 0
|
||||||
var offset_y = node.offset_y || 0
|
var offset_y = node.offset_y || 0
|
||||||
var scale_x = node.scale_x || 1
|
var scale_x = node.scale_x || 1
|
||||||
var scale_y = node.scale_y || 1
|
var scale_y = node.scale_y || 1
|
||||||
var tint = tint_to_color(world_tint, world_opacity)
|
var tint = _tint_to_color(world_tint, world_opacity)
|
||||||
|
|
||||||
for (var x = 0; x < tiles.length; x++) {
|
for (var x = 0; x < tiles.length; x++) {
|
||||||
if (!tiles[x]) continue
|
if (!tiles[x]) continue
|
||||||
@@ -679,7 +380,7 @@ function collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_
|
|||||||
return drawables
|
return drawables
|
||||||
}
|
}
|
||||||
|
|
||||||
function tint_to_color(tint, opacity) {
|
function _tint_to_color(tint, opacity) {
|
||||||
return {
|
return {
|
||||||
r: tint[0],
|
r: tint[0],
|
||||||
g: tint[1],
|
g: tint[1],
|
||||||
@@ -688,37 +389,36 @@ function tint_to_color(tint, opacity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// Batch drawables for efficient rendering
|
||||||
// BATCHING
|
function _batch_drawables(drawables) {
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
function batch_drawables(drawables) {
|
|
||||||
var batches = []
|
var batches = []
|
||||||
var current_batch = null
|
var current_batch = null
|
||||||
|
|
||||||
|
var mat = {blend: 'alpha', sampler: 'nearest'}
|
||||||
|
|
||||||
for (var drawable of drawables) {
|
array.for(drawables, drawable => {
|
||||||
if (drawable.type == 'sprite') {
|
if (drawable.type == 'sprite') {
|
||||||
var texture = drawable.texture || drawable.image
|
var texture = drawable.texture || drawable.image
|
||||||
var material = drawable.material || {blend: 'alpha', sampler: 'nearest'}
|
var material = drawable.material || mat
|
||||||
var scissor = drawable.scissor
|
var scissor = drawable.scissor
|
||||||
|
|
||||||
// Start new batch if texture/material/scissor changed
|
// Check if can merge with current batch
|
||||||
if (!current_batch ||
|
if (current_batch &&
|
||||||
current_batch.type != 'sprite_batch' ||
|
current_batch.type == 'sprite_batch' &&
|
||||||
current_batch.texture != texture ||
|
current_batch.texture == texture &&
|
||||||
!rect_equal(current_batch.scissor, scissor) ||
|
_rect_equal(current_batch.scissor, scissor) &&
|
||||||
!materials_equal(current_batch.material, material)) {
|
_materials_equal(current_batch.material, material)) {
|
||||||
|
current_batch.sprites.push(drawable)
|
||||||
|
} else {
|
||||||
if (current_batch) batches.push(current_batch)
|
if (current_batch) batches.push(current_batch)
|
||||||
current_batch = {
|
current_batch = {
|
||||||
type: 'sprite_batch',
|
type: 'sprite_batch',
|
||||||
texture: texture,
|
texture: texture,
|
||||||
material: material,
|
material: material,
|
||||||
scissor: scissor,
|
scissor: scissor,
|
||||||
sprites: []
|
sprites: [drawable]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current_batch.sprites.push(drawable)
|
|
||||||
} else {
|
} else {
|
||||||
// Non-sprite: flush batch, add individually
|
// Non-sprite: flush batch, add individually
|
||||||
if (current_batch) {
|
if (current_batch) {
|
||||||
@@ -727,20 +427,20 @@ function batch_drawables(drawables) {
|
|||||||
}
|
}
|
||||||
batches.push({type: drawable.type, drawable: drawable, scissor: drawable.scissor})
|
batches.push({type: drawable.type, drawable: drawable, scissor: drawable.scissor})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
if (current_batch) batches.push(current_batch)
|
if (current_batch) batches.push(current_batch)
|
||||||
|
|
||||||
return batches
|
return batches
|
||||||
}
|
}
|
||||||
|
|
||||||
function rect_equal(a, b) {
|
function _rect_equal(a, b) {
|
||||||
if (!a && !b) return true
|
if (!a && !b) return true
|
||||||
if (!a || !b) return false
|
if (!a || !b) return false
|
||||||
return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height
|
return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height
|
||||||
}
|
}
|
||||||
|
|
||||||
function materials_equal(a, b) {
|
function _materials_equal(a, b) {
|
||||||
if (!a || !b) return a == b
|
if (!a || !b) return a == b
|
||||||
return a.blend == b.blend && a.sampler == b.sampler && a.shader == b.shader
|
return a.blend == b.blend && a.sampler == b.sampler && a.shader == b.shader
|
||||||
}
|
}
|
||||||
|
|||||||
86
geometry.c
86
geometry.c
@@ -1200,6 +1200,91 @@ JSC_CCALL(geometry_weave,
|
|||||||
return result;
|
return result;
|
||||||
)
|
)
|
||||||
|
|
||||||
|
JSC_CCALL(geometry_sprite_vertices,
|
||||||
|
JSValue sprite = argv[0];
|
||||||
|
|
||||||
|
// Get sprite properties
|
||||||
|
double x, y, w, h, u0, v0, u1, v1;
|
||||||
|
JSValue x_val = JS_GetPropertyStr(js, sprite, "x");
|
||||||
|
JSValue y_val = JS_GetPropertyStr(js, sprite, "y");
|
||||||
|
JSValue w_val = JS_GetPropertyStr(js, sprite, "w");
|
||||||
|
JSValue h_val = JS_GetPropertyStr(js, sprite, "h");
|
||||||
|
JSValue u0_val = JS_GetPropertyStr(js, sprite, "u0");
|
||||||
|
JSValue v0_val = JS_GetPropertyStr(js, sprite, "v0");
|
||||||
|
JSValue u1_val = JS_GetPropertyStr(js, sprite, "u1");
|
||||||
|
JSValue v1_val = JS_GetPropertyStr(js, sprite, "v1");
|
||||||
|
JSValue c_val = JS_GetPropertyStr(js, sprite, "c");
|
||||||
|
|
||||||
|
JS_ToFloat64(js, &x, x_val);
|
||||||
|
JS_ToFloat64(js, &y, y_val);
|
||||||
|
JS_ToFloat64(js, &w, w_val);
|
||||||
|
JS_ToFloat64(js, &h, h_val);
|
||||||
|
JS_ToFloat64(js, &u0, u0_val);
|
||||||
|
JS_ToFloat64(js, &v0, v0_val);
|
||||||
|
JS_ToFloat64(js, &u1, u1_val);
|
||||||
|
JS_ToFloat64(js, &v1, v1_val);
|
||||||
|
|
||||||
|
HMM_Vec4 c = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||||
|
if (!JS_IsNull(c_val)) {
|
||||||
|
c = js2color(js, c_val);
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_FreeValue(js, x_val);
|
||||||
|
JS_FreeValue(js, y_val);
|
||||||
|
JS_FreeValue(js, w_val);
|
||||||
|
JS_FreeValue(js, h_val);
|
||||||
|
JS_FreeValue(js, u0_val);
|
||||||
|
JS_FreeValue(js, v0_val);
|
||||||
|
JS_FreeValue(js, u1_val);
|
||||||
|
JS_FreeValue(js, v1_val);
|
||||||
|
JS_FreeValue(js, c_val);
|
||||||
|
|
||||||
|
// 4 vertices * 8 floats per vertex (x, y, u, v, r, g, b, a)
|
||||||
|
float vertex_data[4 * 8];
|
||||||
|
|
||||||
|
// v0: bottom-left
|
||||||
|
vertex_data[0] = x;
|
||||||
|
vertex_data[1] = y;
|
||||||
|
vertex_data[2] = u0;
|
||||||
|
vertex_data[3] = v1; // Flip V
|
||||||
|
vertex_data[4] = c.r;
|
||||||
|
vertex_data[5] = c.g;
|
||||||
|
vertex_data[6] = c.b;
|
||||||
|
vertex_data[7] = c.a;
|
||||||
|
|
||||||
|
// v1: bottom-right
|
||||||
|
vertex_data[8] = x + w;
|
||||||
|
vertex_data[9] = y;
|
||||||
|
vertex_data[10] = u1;
|
||||||
|
vertex_data[11] = v1; // Flip V
|
||||||
|
vertex_data[12] = c.r;
|
||||||
|
vertex_data[13] = c.g;
|
||||||
|
vertex_data[14] = c.b;
|
||||||
|
vertex_data[15] = c.a;
|
||||||
|
|
||||||
|
// v2: top-right
|
||||||
|
vertex_data[16] = x + w;
|
||||||
|
vertex_data[17] = y + h;
|
||||||
|
vertex_data[18] = u1;
|
||||||
|
vertex_data[19] = v0; // Flip V
|
||||||
|
vertex_data[20] = c.r;
|
||||||
|
vertex_data[21] = c.g;
|
||||||
|
vertex_data[22] = c.b;
|
||||||
|
vertex_data[23] = c.a;
|
||||||
|
|
||||||
|
// v3: top-left
|
||||||
|
vertex_data[24] = x;
|
||||||
|
vertex_data[25] = y + h;
|
||||||
|
vertex_data[26] = u0;
|
||||||
|
vertex_data[27] = v0; // Flip V
|
||||||
|
vertex_data[28] = c.r;
|
||||||
|
vertex_data[29] = c.g;
|
||||||
|
vertex_data[30] = c.b;
|
||||||
|
vertex_data[31] = c.a;
|
||||||
|
|
||||||
|
return js_new_blob_stoned_copy(js, vertex_data, sizeof(vertex_data));
|
||||||
|
)
|
||||||
|
|
||||||
static const JSCFunctionListEntry js_geometry_funcs[] = {
|
static const JSCFunctionListEntry js_geometry_funcs[] = {
|
||||||
MIST_FUNC_DEF(geometry, rect_intersection, 2),
|
MIST_FUNC_DEF(geometry, rect_intersection, 2),
|
||||||
MIST_FUNC_DEF(geometry, rect_intersects, 2),
|
MIST_FUNC_DEF(geometry, rect_intersects, 2),
|
||||||
@@ -1213,6 +1298,7 @@ static const JSCFunctionListEntry js_geometry_funcs[] = {
|
|||||||
MIST_FUNC_DEF(geometry, rect_transform, 2),
|
MIST_FUNC_DEF(geometry, rect_transform, 2),
|
||||||
MIST_FUNC_DEF(geometry, tilemap_to_data, 1),
|
MIST_FUNC_DEF(geometry, tilemap_to_data, 1),
|
||||||
MIST_FUNC_DEF(geometry, sprites_to_data, 1),
|
MIST_FUNC_DEF(geometry, sprites_to_data, 1),
|
||||||
|
MIST_FUNC_DEF(geometry, sprite_vertices, 1),
|
||||||
MIST_FUNC_DEF(geometry, transform_xy_blob, 2),
|
MIST_FUNC_DEF(geometry, transform_xy_blob, 2),
|
||||||
MIST_FUNC_DEF(gpu, tile, 4),
|
MIST_FUNC_DEF(gpu, tile, 4),
|
||||||
MIST_FUNC_DEF(gpu, slice9, 4),
|
MIST_FUNC_DEF(gpu, slice9, 4),
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ function create_image(path){
|
|||||||
def bytes = io.slurp(path);
|
def bytes = io.slurp(path);
|
||||||
|
|
||||||
var ext = path.split('.').pop()
|
var ext = path.split('.').pop()
|
||||||
let raw = decode_image(bytes, ext);
|
var raw = decode_image(bytes, ext);
|
||||||
|
|
||||||
/* ── Case A: single surface (from make_texture) ────────────── */
|
/* ── Case A: single surface (from make_texture) ────────────── */
|
||||||
if(raw && raw.width && raw.pixels && !isa(raw, array)) {
|
if(raw && raw.width && raw.pixels && !isa(raw, array)) {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ var io = use('cellfs')
|
|||||||
var geometry = use('geometry')
|
var geometry = use('geometry')
|
||||||
var blob = use('blob')
|
var blob = use('blob')
|
||||||
var imgui = use('imgui')
|
var imgui = use('imgui')
|
||||||
var utf8 = use('utf8')
|
|
||||||
var json = use('json')
|
var json = use('json')
|
||||||
|
|
||||||
var os = use('os')
|
var os = use('os')
|
||||||
@@ -223,7 +222,7 @@ function make_shader(sh_file)
|
|||||||
{
|
{
|
||||||
var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
|
var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
|
||||||
if (shader_cache[file]) return shader_cache[file]
|
if (shader_cache[file]) return shader_cache[file]
|
||||||
var refl = json.decode(utf8.decode(io.slurp(`shaders/reflection/${sh_file}.json`)))
|
var refl = json.decode(text(io.slurp(`shaders/reflection/${sh_file}.json`)))
|
||||||
|
|
||||||
var shader = {
|
var shader = {
|
||||||
code: io.slurp(file),
|
code: io.slurp(file),
|
||||||
|
|||||||
81
sdl_gpu.cm
81
sdl_gpu.cm
@@ -647,6 +647,7 @@ function _create_gpu_texture(w, h, pixels) {
|
|||||||
|
|
||||||
function _load_image_file(path) {
|
function _load_image_file(path) {
|
||||||
var bytes = io.slurp(path)
|
var bytes = io.slurp(path)
|
||||||
|
var decoded
|
||||||
if (!bytes) return null
|
if (!bytes) return null
|
||||||
|
|
||||||
var ext = path.split('.').pop().toLowerCase()
|
var ext = path.split('.').pop().toLowerCase()
|
||||||
@@ -663,14 +664,14 @@ function _load_image_file(path) {
|
|||||||
surface = qoi.decode(bytes)
|
surface = qoi.decode(bytes)
|
||||||
break
|
break
|
||||||
case 'gif':
|
case 'gif':
|
||||||
var decoded = gif.decode(bytes)
|
decoded = gif.decode(bytes)
|
||||||
if (decoded && decoded.frames && decoded.frames.length > 0) {
|
if (decoded && decoded.frames && decoded.frames.length > 0) {
|
||||||
surface = decoded.frames[0]
|
surface = decoded.frames[0]
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'ase':
|
case 'ase':
|
||||||
case 'aseprite':
|
case 'aseprite':
|
||||||
var decoded = aseprite.decode(bytes)
|
decoded = aseprite.decode(bytes)
|
||||||
if (decoded && decoded.frames && decoded.frames.length > 0) {
|
if (decoded && decoded.frames && decoded.frames.length > 0) {
|
||||||
surface = decoded.frames[0]
|
surface = decoded.frames[0]
|
||||||
}
|
}
|
||||||
@@ -786,19 +787,21 @@ function _build_sprite_vertices(sprites, camera) {
|
|||||||
var vertices_per_sprite = 4
|
var vertices_per_sprite = 4
|
||||||
var indices_per_sprite = 6
|
var indices_per_sprite = 6
|
||||||
|
|
||||||
var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 4)
|
var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 32)
|
||||||
var index_data = new blob_mod(sprites.length * indices_per_sprite * 2)
|
var index_data = geometry.make_quad_indices(sprites.length)
|
||||||
|
|
||||||
var vertex_count = 0
|
var vertex_count = 0
|
||||||
|
|
||||||
|
var white = {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
|
||||||
for (var s of sprites) {
|
array.for(sprites, s => {
|
||||||
var px = s.pos.x != null ? s.pos.x : (s.pos[0] || 0)
|
var px = s.pos.x
|
||||||
var py = s.pos.y != null ? s.pos.y : (s.pos[1] || 0)
|
var py = s.pos.y
|
||||||
var w = s.width || 1
|
var w = s.width || 1
|
||||||
var h = s.height || 1
|
var h = s.height || 1
|
||||||
var ax = s.anchor_x || 0
|
var ax = s.anchor_x || 0
|
||||||
var ay = s.anchor_y || 0
|
var ay = s.anchor_y || 0
|
||||||
var c = s.color || {r: 1, g: 1, b: 1, a: 1}
|
var c = s.color || white
|
||||||
|
|
||||||
// Apply anchor
|
// Apply anchor
|
||||||
var x = px - w * ax
|
var x = px - w * ax
|
||||||
@@ -809,58 +812,11 @@ function _build_sprite_vertices(sprites, camera) {
|
|||||||
var v0 = s.uv_rect ? s.uv_rect.y : 0
|
var v0 = s.uv_rect ? s.uv_rect.y : 0
|
||||||
var u1 = s.uv_rect ? (s.uv_rect.x + s.uv_rect.width) : 1
|
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
|
var v1 = s.uv_rect ? (s.uv_rect.y + s.uv_rect.height) : 1
|
||||||
|
|
||||||
// Quad vertices (bottom-left, bottom-right, top-right, top-left)
|
// call to implement
|
||||||
// v0: bottom-left
|
var verts = geometry.sprite_vertices({x,y,w,h,u0,v0,u1,v1,c})
|
||||||
vertex_data.wf(x)
|
vertex_data.write_blob(verts)
|
||||||
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 {
|
return {
|
||||||
vertices: stone(vertex_data),
|
vertices: stone(vertex_data),
|
||||||
@@ -1042,6 +998,7 @@ function _execute_commands(commands, window_size) {
|
|||||||
var current_target = null
|
var current_target = null
|
||||||
var current_camera = null
|
var current_camera = null
|
||||||
var pending_draws = []
|
var pending_draws = []
|
||||||
|
var target
|
||||||
|
|
||||||
// Cache swapchain texture for the duration of this command buffer
|
// Cache swapchain texture for the duration of this command buffer
|
||||||
var _swapchain_tex = null
|
var _swapchain_tex = null
|
||||||
@@ -1067,7 +1024,7 @@ function _execute_commands(commands, window_size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start new pass
|
// Start new pass
|
||||||
var target = cmd.target
|
target = cmd.target
|
||||||
var clear = cmd.clear
|
var clear = cmd.clear
|
||||||
|
|
||||||
if (target == 'screen') {
|
if (target == 'screen') {
|
||||||
@@ -1209,7 +1166,7 @@ function _execute_commands(commands, window_size) {
|
|||||||
imgui_mod.prepare(cmd_buffer)
|
imgui_mod.prepare(cmd_buffer)
|
||||||
|
|
||||||
// Restart pass to the same target for rendering
|
// Restart pass to the same target for rendering
|
||||||
var target = cmd.target
|
target = cmd.target
|
||||||
var swap_tex = null
|
var swap_tex = null
|
||||||
if (target == 'screen') {
|
if (target == 'screen') {
|
||||||
swap_tex = get_swapchain_tex()
|
swap_tex = get_swapchain_tex()
|
||||||
|
|||||||
101
sprite.cm
101
sprite.cm
@@ -1,6 +1,10 @@
|
|||||||
// sprite
|
// sprite.cm - Sprite node factory
|
||||||
|
//
|
||||||
|
// Returns a function that creates sprite instances via meme()
|
||||||
|
|
||||||
var sprite = {
|
var sprite = {
|
||||||
pos: {x:0, y:0},
|
type: 'sprite',
|
||||||
|
pos: null,
|
||||||
layer: 0,
|
layer: 0,
|
||||||
image: null,
|
image: null,
|
||||||
width: 1,
|
width: 1,
|
||||||
@@ -9,10 +13,95 @@ var sprite = {
|
|||||||
anchor_y: 0,
|
anchor_y: 0,
|
||||||
scale_x: 1,
|
scale_x: 1,
|
||||||
scale_y: 1,
|
scale_y: 1,
|
||||||
color: {r:1, g:1, b:1, a:1},
|
color: null,
|
||||||
animation: null, // 'walk', 'attack', etc
|
uv_rect: null,
|
||||||
|
slice: null,
|
||||||
|
tile: null,
|
||||||
|
material: null,
|
||||||
|
animation: null,
|
||||||
frame: 0,
|
frame: 0,
|
||||||
opacity: 1
|
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}
|
||||||
|
if (this.pos.x == x && this.pos.y == y) return this
|
||||||
|
this.pos.x = x
|
||||||
|
this.pos.y = y
|
||||||
|
this.dirty |= 1 // TRANSFORM
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
set_image: function(img) {
|
||||||
|
if (this.image == img) return this
|
||||||
|
this.image = img
|
||||||
|
this.dirty |= 2 // CONTENT
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
set_size: function(w, h) {
|
||||||
|
if (this.width == w && this.height == h) return this
|
||||||
|
this.width = w
|
||||||
|
this.height = h
|
||||||
|
this.dirty |= 2 // CONTENT
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
set_anchor: function(x, y) {
|
||||||
|
if (this.anchor_x == x && this.anchor_y == y) return this
|
||||||
|
this.anchor_x = x
|
||||||
|
this.anchor_y = y
|
||||||
|
this.dirty |= 1 // 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 |= 2 // CONTENT
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
set_opacity: function(o) {
|
||||||
|
if (this.opacity == o) return this
|
||||||
|
this.opacity = o
|
||||||
|
this.dirty |= 2 // CONTENT
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user