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) {
|
||||
// Merge array of configs from right to left (right overrides left)
|
||||
// And merge with base_config
|
||||
var res = meme(base_config)
|
||||
for (var c of configs) {
|
||||
if (c) res = meme(res, c)
|
||||
}
|
||||
return res
|
||||
return meme(base_config, ...configs)
|
||||
}
|
||||
|
||||
function push_node(configs, contain_mode) {
|
||||
|
||||
636
compositor.cm
636
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.
|
||||
// It handles:
|
||||
// - Renderer selection per layer (film2d, forward3d, imgui, retro3d...)
|
||||
// - Camera + viewport binding per pass
|
||||
// - Target ownership (composition root, fixed resolution, effect islands)
|
||||
// - Presentation modes (stretch, letterbox, integer_scale, etc.)
|
||||
// - Global post effects (CRT, etc.)
|
||||
// - Layer stacking order
|
||||
// The compositor is the SINGLE OWNER of all effects.
|
||||
// It takes scene descriptions and produces abstract render plans.
|
||||
//
|
||||
// Architecture:
|
||||
// Scene Tree (Retained) -> Compositor (Effect orchestration) -> Render Plan -> Backend
|
||||
//
|
||||
// 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.
|
||||
|
||||
// ========================================================================
|
||||
// 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 effects_mod = use('effects')
|
||||
|
||||
var compositor = {}
|
||||
|
||||
// Default renderer capabilities (conservative - requires islands for everything)
|
||||
var DEFAULT_CAPS = {
|
||||
supports_mask_stencil: true,
|
||||
supports_mask_alpha: true,
|
||||
supports_bloom: true,
|
||||
supports_blur: true
|
||||
// Presentation modes
|
||||
compositor.PRESENTATION = {
|
||||
DISABLED: 'disabled',
|
||||
STRETCH: 'stretch',
|
||||
LETTERBOX: 'letterbox',
|
||||
OVERSCAN: 'overscan',
|
||||
INTEGER_SCALE: 'integer_scale'
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// COMPILATION: Turn composition tree into render plan
|
||||
// ========================================================================
|
||||
// Blend modes
|
||||
compositor.BLEND = {
|
||||
REPLACE: 'replace',
|
||||
OVER: 'over',
|
||||
ADD: 'add'
|
||||
}
|
||||
|
||||
// Compile a composition into a render plan
|
||||
// comp: composition description (the DSL)
|
||||
// renderers: map of renderer_type -> renderer module
|
||||
// backend: the render backend (sdl_gpu, etc.)
|
||||
// Compile a composition tree into a render plan
|
||||
compositor.compile = function(comp, renderers, backend) {
|
||||
var ctx = {
|
||||
renderers: renderers,
|
||||
backend: backend,
|
||||
passes: [],
|
||||
targets: {},
|
||||
persistent_targets: {},
|
||||
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
|
||||
ctx.screen_size = backend.get_window_size() || {width: 1280, height: 720}
|
||||
// Get screen size from backend
|
||||
ctx.screen_size = backend.get_window_size ? backend.get_window_size() : {width: 1280, height: 720}
|
||||
if (!ctx.screen_size.width) ctx.screen_size.width = 1280
|
||||
if (!ctx.screen_size.height) ctx.screen_size.height = 720
|
||||
|
||||
// Compile the composition tree
|
||||
var result = compile_node(comp, ctx, null, ctx.screen_size)
|
||||
// Normalize to w/h
|
||||
ctx.screen_size.w = ctx.screen_size.width
|
||||
ctx.screen_size.h = ctx.screen_size.height
|
||||
|
||||
// Compile the tree
|
||||
_compile_node(comp, ctx, null, ctx.screen_size)
|
||||
|
||||
return {
|
||||
passes: ctx.passes,
|
||||
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
|
||||
function compile_node(node, ctx, parent_target, parent_size) {
|
||||
// Compile a single node
|
||||
function _compile_node(node, ctx, parent_target, parent_size) {
|
||||
if (!node) return {output: null}
|
||||
|
||||
var node_type = node.type
|
||||
|
||||
// Determine if this node owns a target
|
||||
var owns_target = false
|
||||
var target = parent_target
|
||||
var target_size = parent_size
|
||||
|
||||
// Target ownership rules:
|
||||
// 1. Composition root (target: "screen")
|
||||
// 2. Node has explicit resolution
|
||||
// 3. Node has effects that require isolation
|
||||
var target_size = {w: parent_size.w || parent_size.width, h: parent_size.h || parent_size.height}
|
||||
|
||||
// Determine target ownership
|
||||
if (node.target == 'screen') {
|
||||
owns_target = true
|
||||
target = 'screen'
|
||||
target_size = ctx.screen_size
|
||||
target_size = {w: ctx.screen_size.w, h: ctx.screen_size.h}
|
||||
} else if (node.resolution) {
|
||||
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 = allocate_target(ctx, target_size, node.name || 'res_target')
|
||||
} else if (node.effects && effects_require_island(node.effects, ctx)) {
|
||||
target_size = {w: node.resolution.w || node.resolution.width, h: node.resolution.h || node.resolution.height}
|
||||
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'res_target')
|
||||
} else if (node.effects && _effects_require_target(node.effects, ctx)) {
|
||||
owns_target = true
|
||||
target_size = parent_size || ctx.screen_size
|
||||
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')
|
||||
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'effect_target')
|
||||
}
|
||||
|
||||
// Handle clear for target owners
|
||||
ctx.target_size = target_size
|
||||
|
||||
// Handle clear
|
||||
if (owns_target && node.clear) {
|
||||
ctx.passes.push({
|
||||
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') {
|
||||
// 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') {
|
||||
// Group with potential effects - process layers
|
||||
return compile_group(node, ctx, target, target_size, parent_target, parent_size)
|
||||
} else if (is_renderer_type(node_type)) {
|
||||
// Renderer layer (film2d, forward3d, imgui, etc.)
|
||||
return compile_renderer_layer(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)) {
|
||||
return _compile_renderer(node, ctx, target, target_size, parent_target, parent_size)
|
||||
} 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({
|
||||
type: 'imgui',
|
||||
target: target,
|
||||
@@ -151,57 +132,49 @@ function compile_node(node, ctx, parent_target, parent_size) {
|
||||
return {output: target}
|
||||
}
|
||||
|
||||
// Compile a composition (root or nested)
|
||||
function compile_composition(node, ctx, target, target_size) {
|
||||
// Compile composition root
|
||||
function _compile_composition(node, ctx, target, target_size) {
|
||||
var layers = node.layers || []
|
||||
|
||||
for (var i = 0; i < layers.length; i++) {
|
||||
var layer = layers[i]
|
||||
var is_first = (i == 0)
|
||||
|
||||
// Set default blend based on position
|
||||
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}
|
||||
}
|
||||
|
||||
// Compile a group (may have effects)
|
||||
function compile_group(node, ctx, target, target_size, parent_target, parent_size) {
|
||||
// Compile group with effects
|
||||
function _compile_group(node, ctx, target, target_size, parent_target, parent_size) {
|
||||
var layers = node.layers || []
|
||||
var original_target = target
|
||||
|
||||
// Process child layers into this target
|
||||
// Process child layers
|
||||
for (var i = 0; i < layers.length; i++) {
|
||||
var layer = layers[i]
|
||||
var is_first = (i == 0)
|
||||
|
||||
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
|
||||
if (node.effects) {
|
||||
for (var effect of node.effects) {
|
||||
effect._node_id = node.name || `node_${ctx.target_counter}`
|
||||
target = compile_effect(effect, ctx, target, target_size)
|
||||
// Apply effects - this is where the compositor owns all effect logic
|
||||
if (node.effects && node.effects.length > 0) {
|
||||
for (var j = 0; j < node.effects.length; j++) {
|
||||
var effect = node.effects[j]
|
||||
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
|
||||
if (needs_composite) {
|
||||
var presentation = node.presentation || 'disabled'
|
||||
var blend = node.blend || 'over'
|
||||
|
||||
// If parent is screen, use blit_to_screen pass type
|
||||
if (parent_target == 'screen') {
|
||||
ctx.passes.push({
|
||||
type: 'blit_to_screen',
|
||||
@@ -228,28 +201,28 @@ function compile_group(node, ctx, target, target_size, parent_target, parent_siz
|
||||
return {output: target}
|
||||
}
|
||||
|
||||
// Compile a renderer layer (film2d, forward3d, etc.)
|
||||
function compile_renderer_layer(node, ctx, target, target_size, parent_target, parent_size) {
|
||||
// Compile renderer layer (film2d, forward3d, etc.)
|
||||
function _compile_renderer(node, ctx, target, target_size, parent_target, parent_size) {
|
||||
var renderer_type = node.type
|
||||
var renderer = ctx.renderers[renderer_type]
|
||||
|
||||
if (!renderer) {
|
||||
log.console(`compositor: Unknown renderer type: ${renderer_type}`)
|
||||
log.console(`compositor: Unknown renderer: ${renderer_type}`)
|
||||
return {output: target}
|
||||
}
|
||||
|
||||
// Determine if this layer owns its own target
|
||||
var layer_target = target
|
||||
var layer_size = target_size
|
||||
var owns_target = false
|
||||
|
||||
// Check for resolution override
|
||||
if (node.resolution) {
|
||||
owns_target = true
|
||||
layer_size = {width: node.resolution.w, height: node.resolution.h}
|
||||
layer_target = allocate_target(ctx, layer_size, node.name || renderer_type + '_target')
|
||||
layer_size = {w: node.resolution.w, h: node.resolution.h}
|
||||
layer_target = ctx.alloc_target(layer_size.w, layer_size.h, node.name || renderer_type + '_target')
|
||||
}
|
||||
|
||||
// Handle clear for target owners
|
||||
// Clear if we own target
|
||||
if (owns_target && node.clear) {
|
||||
ctx.passes.push({
|
||||
type: 'clear',
|
||||
@@ -270,7 +243,7 @@ function compile_renderer_layer(node, ctx, target, target_size, parent_target, p
|
||||
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) {
|
||||
var presentation = node.presentation || 'disabled'
|
||||
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}
|
||||
}
|
||||
|
||||
// Compile an effect
|
||||
function compile_effect(effect, ctx, input_target, target_size) {
|
||||
// Compile an effect using the effect registry
|
||||
function _compile_effect(effect, ctx, input_target, target_size, node_id) {
|
||||
var effect_type = effect.type
|
||||
var effect_def = effects_mod.get(effect_type)
|
||||
|
||||
if (effect_type == 'crt') {
|
||||
var output = allocate_target(ctx, target_size, 'crt_output')
|
||||
ctx.passes.push({
|
||||
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') {
|
||||
return compile_bloom_effect(effect, ctx, input_target, target_size)
|
||||
}
|
||||
|
||||
if (effect_type == 'mask') {
|
||||
return compile_mask_effect(effect, ctx, input_target, target_size)
|
||||
}
|
||||
|
||||
if (effect_type == 'blur') {
|
||||
return compile_blur_effect(effect, ctx, input_target, target_size)
|
||||
}
|
||||
|
||||
if (effect_type == 'accumulator') {
|
||||
return compile_accumulator_effect(effect, ctx, input_target, target_size)
|
||||
}
|
||||
|
||||
if (!effect_def) {
|
||||
log.console(`compositor: Unknown effect: ${effect_type}`)
|
||||
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
|
||||
// Build effect context
|
||||
var effect_ctx = {
|
||||
backend: ctx.backend,
|
||||
target_size: {w: target_size.w, h: target_size.h},
|
||||
alloc_target: function(w, h, hint) {
|
||||
return ctx.alloc_target(w, h, hint)
|
||||
},
|
||||
get_persistent_target: function(key, w, h) {
|
||||
return ctx.get_persistent_target(node_id + '_' + key, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
// Composite bloom back onto input
|
||||
var output = allocate_target(ctx, target_size, 'bloom_output')
|
||||
ctx.passes.push({
|
||||
// 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: input_target,
|
||||
overlay: blur_src,
|
||||
output: output,
|
||||
mode: 'add'
|
||||
})
|
||||
|
||||
return output
|
||||
base: ep.base,
|
||||
overlay: ep.overlay,
|
||||
output: ep.output,
|
||||
mode: ep.blend || 'over'
|
||||
}
|
||||
|
||||
// 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({
|
||||
case 'blit':
|
||||
return {
|
||||
type: 'composite',
|
||||
source: accum_curr,
|
||||
dest: accum_prev,
|
||||
source_size: target_size,
|
||||
dest_size: target_size,
|
||||
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'
|
||||
})
|
||||
|
||||
return accum_curr
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PRESENTATION HELPERS
|
||||
// ========================================================================
|
||||
// Check if type is a renderer
|
||||
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) {
|
||||
var sw = source_size.width
|
||||
var sh = source_size.height
|
||||
var dw = dest_size.width
|
||||
var dh = dest_size.height
|
||||
var sw = source_size.w || source_size.width
|
||||
var sh = source_size.h || source_size.height
|
||||
var dw = dest_size.w || dest_size.width
|
||||
var dh = dest_size.h || dest_size.height
|
||||
|
||||
if (mode == 'disabled') return {x: 0, y: 0, width: number.min(sw,dw), height: number.min(sh,dh)}
|
||||
if (mode == 'stretch') return {x: 0, y: 0, width: dw, height: dh}
|
||||
if (mode == 'disabled') {
|
||||
return {x: 0, y: 0, width: number.min(sw, dw), height: number.min(sh, dh)}
|
||||
}
|
||||
if (mode == 'stretch') {
|
||||
return {x: 0, y: 0, width: dw, height: dh}
|
||||
}
|
||||
|
||||
var src_aspect = sw / sh
|
||||
var dst_aspect = dw / dh
|
||||
|
||||
if (mode == 'letterbox') {
|
||||
var scale = src_aspect > dst_aspect
|
||||
? dw / sw
|
||||
: dh / sh
|
||||
var scale = src_aspect > dst_aspect ? dw / sw : dh / sh
|
||||
var w = sw * scale
|
||||
var h = sh * scale
|
||||
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
||||
}
|
||||
|
||||
if (mode == 'overscan') {
|
||||
var scale = src_aspect > dst_aspect
|
||||
? dh / sh
|
||||
: dw / sw
|
||||
var scale = src_aspect > dst_aspect ? dh / sh : dw / sw
|
||||
var w = sw * scale
|
||||
var h = sh * scale
|
||||
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
// Default: no scaling
|
||||
return {x: 0, y: 0, width: sw, height: sh}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PLAN EXECUTION
|
||||
// ========================================================================
|
||||
|
||||
// Execute a compiled render plan
|
||||
compositor.execute = function(plan, renderers, backend) {
|
||||
var target_cache = {}
|
||||
|
||||
// Pre-allocate all targets
|
||||
// Pre-allocate targets
|
||||
for (var key in plan.targets) {
|
||||
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
|
||||
@@ -616,18 +429,21 @@ compositor.execute = function(plan, renderers, backend) {
|
||||
// Execute passes
|
||||
var commands = []
|
||||
|
||||
for (var pass of plan.passes) {
|
||||
var pass_commands = execute_pass(pass, renderers, backend, resolve_target)
|
||||
for (var cmd of pass_commands) {
|
||||
commands.push(cmd)
|
||||
for (var i = 0; i < plan.passes.length; i++) {
|
||||
var pass = plan.passes[i]
|
||||
var pass_cmds = _execute_pass(pass, renderers, backend, resolve_target, compositor)
|
||||
for (var j = 0; j < pass_cmds.length; j++) {
|
||||
commands.push(pass_cmds[j])
|
||||
}
|
||||
}
|
||||
|
||||
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 source
|
||||
|
||||
switch (pass.type) {
|
||||
case 'clear':
|
||||
@@ -640,7 +456,7 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
||||
var renderer = renderers[pass.renderer]
|
||||
if (renderer && renderer.render) {
|
||||
var target = resolve_target(pass.target)
|
||||
var render_result = renderer.render({
|
||||
var result = renderer.render({
|
||||
root: pass.root,
|
||||
camera: pass.camera,
|
||||
target: target,
|
||||
@@ -648,9 +464,9 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
||||
blend: pass.blend,
|
||||
clear: pass.clear
|
||||
}, backend)
|
||||
if (render_result && render_result.commands) {
|
||||
for (var cmd of render_result.commands) {
|
||||
commands.push(cmd)
|
||||
if (result && result.commands) {
|
||||
for (var i = 0; i < result.commands.length; i++) {
|
||||
commands.push(result.commands[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -659,10 +475,10 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
||||
case 'shader_pass':
|
||||
var input = resolve_target(pass.input)
|
||||
var output = resolve_target(pass.output)
|
||||
var extra_inputs = []
|
||||
var extra = []
|
||||
if (pass.extra_inputs) {
|
||||
for (var t of pass.extra_inputs) {
|
||||
extra_inputs.push(resolve_target(t))
|
||||
for (var i = 0; i < pass.extra_inputs.length; i++) {
|
||||
extra.push(resolve_target(pass.extra_inputs[i]))
|
||||
}
|
||||
}
|
||||
commands.push({
|
||||
@@ -670,15 +486,15 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
||||
shader: pass.shader,
|
||||
input: input,
|
||||
output: output,
|
||||
extra_inputs: extra_inputs,
|
||||
extra_inputs: extra,
|
||||
uniforms: pass.uniforms
|
||||
})
|
||||
break
|
||||
|
||||
case 'composite':
|
||||
var source = resolve_target(pass.source)
|
||||
case 'composite': {
|
||||
source = resolve_target(pass.source)
|
||||
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) {
|
||||
rect.x += (pass.pos.x || 0)
|
||||
rect.y += (pass.pos.y || 0)
|
||||
@@ -691,11 +507,12 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
||||
dst_rect: rect,
|
||||
filter: filter
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'blit_to_screen':
|
||||
var source = resolve_target(pass.source)
|
||||
var rect = compositor.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
|
||||
case 'blit_to_screen': {
|
||||
source = resolve_target(pass.source)
|
||||
var rect = comp.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
|
||||
if (pass.pos) {
|
||||
rect.x += (pass.pos.x || 0)
|
||||
rect.y += (pass.pos.y || 0)
|
||||
@@ -708,9 +525,10 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
||||
dst_rect: rect,
|
||||
filter: filter
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'composite_textures':
|
||||
case 'composite_textures': {
|
||||
var base = resolve_target(pass.base)
|
||||
var overlay = resolve_target(pass.overlay)
|
||||
var output = resolve_target(pass.output)
|
||||
@@ -721,9 +539,10 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
||||
output: output,
|
||||
mode: pass.mode || 'over'
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'apply_mask':
|
||||
case 'apply_mask': {
|
||||
var content = resolve_target(pass.content)
|
||||
var mask = resolve_target(pass.mask)
|
||||
var output = resolve_target(pass.output)
|
||||
@@ -735,34 +554,35 @@ function execute_pass(pass, renderers, backend, resolve_target) {
|
||||
mode: pass.mode,
|
||||
invert: pass.invert
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'imgui':
|
||||
case 'imgui': {
|
||||
commands.push({
|
||||
cmd: 'imgui',
|
||||
draw: pass.draw,
|
||||
rect: pass.rect,
|
||||
target: resolve_target(pass.target)
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'render_mask_source':
|
||||
// This would render the mask source node
|
||||
// For now, delegate to film2d renderer
|
||||
case 'render_mask_source': {
|
||||
var target = resolve_target(pass.target)
|
||||
var renderer = renderers['film2d']
|
||||
if (renderer && renderer.render) {
|
||||
var render_result = renderer.render({
|
||||
var result = renderer.render({
|
||||
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_size: pass.target_size,
|
||||
blend: 'replace',
|
||||
clear: {r: 0, g: 0, b: 0, a: 0}
|
||||
}, backend)
|
||||
if (render_result && render_result.commands) {
|
||||
for (var cmd of render_result.commands) {
|
||||
commands.push(cmd)
|
||||
if (result && result.commands) {
|
||||
for (var i = 0; i < result.commands.length; i++) {
|
||||
commands.push(result.commands[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// This is the "how to draw a plate" module - it knows about sprites, tilemaps,
|
||||
// text, particles, etc.
|
||||
// This is the "how to draw a 2D plate" module.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// Scene-level effects (bloom, mask, blur on groups) are handled here:
|
||||
// - Effects that can be done inline (stencil mask) are done without extra targets
|
||||
// - 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.)
|
||||
// This module does NOT handle effects - that's compositor territory.
|
||||
// It only knows about sprites, tilemaps, text, particles, and rects.
|
||||
|
||||
var film2d = {}
|
||||
|
||||
// Renderer capabilities - what this renderer can do inline vs requiring islands
|
||||
// Renderer capabilities
|
||||
film2d.capabilities = {
|
||||
supports_mask_stencil: true, // Hard masks via stencil (no target needed)
|
||||
supports_mask_alpha: true, // Soft masks (needs target)
|
||||
supports_bloom: true, // Bloom effect (needs target)
|
||||
supports_blur: true // Blur effect (needs target)
|
||||
supports_mask_stencil: true,
|
||||
supports_mask_alpha: true,
|
||||
supports_bloom: true,
|
||||
supports_blur: true
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 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.)
|
||||
// Main render function
|
||||
film2d.render = function(params, backend) {
|
||||
var root = params.root
|
||||
var camera = params.camera
|
||||
@@ -42,36 +29,31 @@ film2d.render = function(params, backend) {
|
||||
|
||||
if (!root) return {commands: []}
|
||||
|
||||
// Context for effect processing
|
||||
var ctx = {
|
||||
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)
|
||||
// Collect all drawables from scene tree
|
||||
var drawables = _collect_drawables(root, camera, null, null, null, null)
|
||||
|
||||
// Sort by layer, then by Y for depth sorting
|
||||
log.console(drawables.length)
|
||||
drawables.sort(function(a, b) {
|
||||
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
|
||||
})
|
||||
|
||||
// Build render commands
|
||||
var commands = ctx.commands
|
||||
var commands = []
|
||||
commands.push({cmd: 'begin_render', target: target, clear: clear_color})
|
||||
commands.push({cmd: 'set_camera', camera: camera})
|
||||
|
||||
// Batch and emit draw commands
|
||||
var batches = batch_drawables(drawables)
|
||||
var batches = _batch_drawables(drawables)
|
||||
var current_scissor = null
|
||||
|
||||
for (var batch of batches) {
|
||||
// Emit scissor command if changed
|
||||
if (!rect_equal(current_scissor, batch.scissor)) {
|
||||
for (var i = 0; i < batches.length; i++) {
|
||||
var batch = batches[i]
|
||||
|
||||
// Emit scissor if changed
|
||||
if (!_rect_equal(current_scissor, batch.scissor)) {
|
||||
commands.push({cmd: 'scissor', rect: batch.scissor})
|
||||
current_scissor = batch.scissor
|
||||
}
|
||||
@@ -102,15 +84,6 @@ film2d.render = function(params, backend) {
|
||||
texture: batch.texture,
|
||||
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}
|
||||
}
|
||||
|
||||
// Process scene tree, handling effects on groups
|
||||
// Returns drawables for the current level, may emit commands for effect islands
|
||||
function process_scene_tree(node, camera, ctx, parent_tint, parent_opacity, parent_scissor, parent_pos) {
|
||||
// Collect drawables from scene tree
|
||||
function _collect_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos) {
|
||||
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_opacity = parent_opacity != null ? parent_opacity : 1
|
||||
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 = []
|
||||
|
||||
// Compute absolute position
|
||||
parent_pos = parent_pos || {x: 0, y: 0}
|
||||
var node_pos = node.pos || {x: 0, y: 0}
|
||||
var abs_x = parent_pos.x + (node_pos.x != null ? node_pos.x : (node_pos[0] || 0))
|
||||
var abs_y = parent_pos.y + (node_pos.y != null ? node_pos.y : (node_pos[1] || 0))
|
||||
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
|
||||
@@ -404,7 +122,6 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
||||
var current_scissor = parent_scissor
|
||||
if (node.scissor) {
|
||||
if (parent_scissor) {
|
||||
// Intersect parent and node scissor
|
||||
var x1 = Math.max(parent_scissor.x, node.scissor.x)
|
||||
var y1 = Math.max(parent_scissor.y, node.scissor.y)
|
||||
var x2 = Math.min(parent_scissor.x + parent_scissor.width, node.scissor.x + node.scissor.width)
|
||||
@@ -417,8 +134,10 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
||||
|
||||
// Handle different node types
|
||||
if (node.type == 'sprite' || (node.image && !node.type)) {
|
||||
var sprite_drawables = collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor)
|
||||
for (var d of sprite_drawables) drawables.push(d)
|
||||
var sprite_drawables = _collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor)
|
||||
for (var i = 0; i < sprite_drawables.length; i++) {
|
||||
drawables.push(sprite_drawables[i])
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type == 'text') {
|
||||
@@ -436,7 +155,7 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
||||
outline_color: node.outline_color,
|
||||
anchor_x: node.anchor_x,
|
||||
anchor_y: node.anchor_y,
|
||||
color: tint_to_color(world_tint, world_opacity),
|
||||
color: _tint_to_color(world_tint, world_opacity),
|
||||
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},
|
||||
width: node.width || 1,
|
||||
height: node.height || 1,
|
||||
color: tint_to_color(world_tint, world_opacity),
|
||||
color: _tint_to_color(world_tint, world_opacity),
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
|
||||
if (node.type == 'particles' || node.particles) {
|
||||
var particles = node.particles || []
|
||||
for (var p of particles) {
|
||||
for (var i = 0; i < particles.length; i++) {
|
||||
var p = particles[i]
|
||||
var px = p.pos ? p.pos.x : 0
|
||||
var py = p.pos ? p.pos.y : 0
|
||||
|
||||
@@ -471,7 +191,7 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
||||
height: (node.height || 1) * (p.scale || 1),
|
||||
anchor_x: 0.5,
|
||||
anchor_y: 0.5,
|
||||
color: p.color || tint_to_color(world_tint, world_opacity),
|
||||
color: p.color || _tint_to_color(world_tint, world_opacity),
|
||||
material: node.material,
|
||||
scissor: current_scissor
|
||||
})
|
||||
@@ -479,20 +199,19 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
|
||||
}
|
||||
|
||||
if (node.type == 'tilemap' || node.tiles) {
|
||||
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)
|
||||
var tile_drawables = _collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor)
|
||||
for (var i = 0; i < tile_drawables.length; i++) {
|
||||
drawables.push(tile_drawables[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse children - use process_scene_tree if ctx available (handles effects)
|
||||
// Recurse children
|
||||
if (node.children) {
|
||||
for (var child of node.children) {
|
||||
var child_drawables
|
||||
if (ctx) {
|
||||
child_drawables = process_scene_tree(child, camera, ctx, world_tint, world_opacity, current_scissor, current_pos)
|
||||
} else {
|
||||
child_drawables = collect_drawables(child, camera, world_tint, world_opacity, current_scissor, current_pos, null)
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
var child_drawables = _collect_drawables(node.children[i], camera, world_tint, world_opacity, current_scissor, current_pos)
|
||||
for (var j = 0; j < child_drawables.length; j++) {
|
||||
drawables.push(child_drawables[j])
|
||||
}
|
||||
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)
|
||||
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 = []
|
||||
|
||||
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 ax = node.anchor_x || 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) {
|
||||
drawables.push({
|
||||
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) {
|
||||
var tx = tile_size ? (tile_size.x || tile_size) : rect.width
|
||||
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 y0 = abs_y - h * ay
|
||||
|
||||
if (node.slice) {
|
||||
// 9-Slice logic
|
||||
// 9-slice
|
||||
var s = node.slice
|
||||
var L = s.left != null ? s.left : (typeof s == 'number' ? s : 0)
|
||||
var R = s.right != null ? s.right : (typeof s == 'number' ? s : 0)
|
||||
@@ -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 stretch = s.stretch != null ? s.stretch : node.stretch
|
||||
|
||||
var Sx = stretch != null ? (stretch.x || stretch) : w
|
||||
var Sy = stretch != null ? (stretch.y || stretch) : h
|
||||
|
||||
// World sizes of borders
|
||||
var WL = L * Sx
|
||||
var WR = R * Sx
|
||||
var HT = T * Sy
|
||||
var HB = B * Sy
|
||||
|
||||
// Middle areas
|
||||
var WM = w - WL - WR
|
||||
var HM = h - HT - HB
|
||||
|
||||
// UV mid dimensions
|
||||
var UM = 1 - L - R
|
||||
var VM = 1 - T - B
|
||||
|
||||
// Natural tile sizes for middle parts
|
||||
var TW = stretch != null ? UM * Sx : WM
|
||||
var TH = stretch != null ? VM * Sy : HM
|
||||
|
||||
// TL
|
||||
// TL, TM, TR
|
||||
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})
|
||||
// TR
|
||||
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})
|
||||
// MM
|
||||
emit_tiled({x: x0 + WL, y: y0 + HT, width: WM, height: HM}, {x:L, y:T, width: UM, height: VM}, {x: TW, y: TH})
|
||||
// MR
|
||||
emit_tiled({x: x0 + WL + WM, y: y0 + HT, width: WR, height: HM}, {x: 1-R, y:T, width: R, height: VM}, {x: WR, y: TH})
|
||||
|
||||
// BL
|
||||
// BL, BM, BR
|
||||
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})
|
||||
// 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})
|
||||
|
||||
} else if (node.tile) {
|
||||
// Full sprite tiling
|
||||
emit_tiled({x: x0, y: y0, width: w, height: h}, {x:0, y:0, width: 1, height: 1}, node.tile)
|
||||
} else {
|
||||
// Normal sprite
|
||||
@@ -640,14 +341,14 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s
|
||||
}
|
||||
|
||||
// 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 tiles = node.tiles || []
|
||||
var offset_x = node.offset_x || 0
|
||||
var offset_y = node.offset_y || 0
|
||||
var scale_x = node.scale_x || 1
|
||||
var scale_y = node.scale_y || 1
|
||||
var tint = tint_to_color(world_tint, world_opacity)
|
||||
var tint = _tint_to_color(world_tint, world_opacity)
|
||||
|
||||
for (var x = 0; x < tiles.length; x++) {
|
||||
if (!tiles[x]) continue
|
||||
@@ -679,7 +380,7 @@ function collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_
|
||||
return drawables
|
||||
}
|
||||
|
||||
function tint_to_color(tint, opacity) {
|
||||
function _tint_to_color(tint, opacity) {
|
||||
return {
|
||||
r: tint[0],
|
||||
g: tint[1],
|
||||
@@ -688,37 +389,36 @@ function tint_to_color(tint, opacity) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// BATCHING
|
||||
// ========================================================================
|
||||
|
||||
function batch_drawables(drawables) {
|
||||
// Batch drawables for efficient rendering
|
||||
function _batch_drawables(drawables) {
|
||||
var batches = []
|
||||
var current_batch = null
|
||||
|
||||
for (var drawable of drawables) {
|
||||
var mat = {blend: 'alpha', sampler: 'nearest'}
|
||||
|
||||
array.for(drawables, drawable => {
|
||||
if (drawable.type == 'sprite') {
|
||||
var texture = drawable.texture || drawable.image
|
||||
var material = drawable.material || {blend: 'alpha', sampler: 'nearest'}
|
||||
var material = drawable.material || mat
|
||||
var scissor = drawable.scissor
|
||||
|
||||
// Start new batch if texture/material/scissor changed
|
||||
if (!current_batch ||
|
||||
current_batch.type != 'sprite_batch' ||
|
||||
current_batch.texture != texture ||
|
||||
!rect_equal(current_batch.scissor, scissor) ||
|
||||
!materials_equal(current_batch.material, material)) {
|
||||
// Check if can merge with current batch
|
||||
if (current_batch &&
|
||||
current_batch.type == 'sprite_batch' &&
|
||||
current_batch.texture == texture &&
|
||||
_rect_equal(current_batch.scissor, scissor) &&
|
||||
_materials_equal(current_batch.material, material)) {
|
||||
current_batch.sprites.push(drawable)
|
||||
} else {
|
||||
if (current_batch) batches.push(current_batch)
|
||||
current_batch = {
|
||||
type: 'sprite_batch',
|
||||
texture: texture,
|
||||
material: material,
|
||||
scissor: scissor,
|
||||
sprites: []
|
||||
sprites: [drawable]
|
||||
}
|
||||
}
|
||||
|
||||
current_batch.sprites.push(drawable)
|
||||
} else {
|
||||
// Non-sprite: flush batch, add individually
|
||||
if (current_batch) {
|
||||
@@ -727,20 +427,20 @@ function batch_drawables(drawables) {
|
||||
}
|
||||
batches.push({type: drawable.type, drawable: drawable, scissor: drawable.scissor})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (current_batch) batches.push(current_batch)
|
||||
|
||||
return batches
|
||||
}
|
||||
|
||||
function rect_equal(a, b) {
|
||||
function _rect_equal(a, b) {
|
||||
if (!a && !b) return true
|
||||
if (!a || !b) return false
|
||||
return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height
|
||||
}
|
||||
|
||||
function materials_equal(a, b) {
|
||||
function _materials_equal(a, b) {
|
||||
if (!a || !b) return a == b
|
||||
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;
|
||||
)
|
||||
|
||||
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[] = {
|
||||
MIST_FUNC_DEF(geometry, rect_intersection, 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, tilemap_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(gpu, tile, 4),
|
||||
MIST_FUNC_DEF(gpu, slice9, 4),
|
||||
|
||||
@@ -158,7 +158,7 @@ function create_image(path){
|
||||
def bytes = io.slurp(path);
|
||||
|
||||
var ext = path.split('.').pop()
|
||||
let raw = decode_image(bytes, ext);
|
||||
var raw = decode_image(bytes, ext);
|
||||
|
||||
/* ── Case A: single surface (from make_texture) ────────────── */
|
||||
if(raw && raw.width && raw.pixels && !isa(raw, array)) {
|
||||
|
||||
@@ -9,7 +9,6 @@ var io = use('cellfs')
|
||||
var geometry = use('geometry')
|
||||
var blob = use('blob')
|
||||
var imgui = use('imgui')
|
||||
var utf8 = use('utf8')
|
||||
var json = use('json')
|
||||
|
||||
var os = use('os')
|
||||
@@ -223,7 +222,7 @@ function make_shader(sh_file)
|
||||
{
|
||||
var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
|
||||
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 = {
|
||||
code: io.slurp(file),
|
||||
|
||||
79
sdl_gpu.cm
79
sdl_gpu.cm
@@ -647,6 +647,7 @@ function _create_gpu_texture(w, h, pixels) {
|
||||
|
||||
function _load_image_file(path) {
|
||||
var bytes = io.slurp(path)
|
||||
var decoded
|
||||
if (!bytes) return null
|
||||
|
||||
var ext = path.split('.').pop().toLowerCase()
|
||||
@@ -663,14 +664,14 @@ function _load_image_file(path) {
|
||||
surface = qoi.decode(bytes)
|
||||
break
|
||||
case 'gif':
|
||||
var decoded = gif.decode(bytes)
|
||||
decoded = gif.decode(bytes)
|
||||
if (decoded && decoded.frames && decoded.frames.length > 0) {
|
||||
surface = decoded.frames[0]
|
||||
}
|
||||
break
|
||||
case 'ase':
|
||||
case 'aseprite':
|
||||
var decoded = aseprite.decode(bytes)
|
||||
decoded = aseprite.decode(bytes)
|
||||
if (decoded && decoded.frames && decoded.frames.length > 0) {
|
||||
surface = decoded.frames[0]
|
||||
}
|
||||
@@ -786,19 +787,21 @@ function _build_sprite_vertices(sprites, camera) {
|
||||
var vertices_per_sprite = 4
|
||||
var indices_per_sprite = 6
|
||||
|
||||
var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 4)
|
||||
var index_data = new blob_mod(sprites.length * indices_per_sprite * 2)
|
||||
var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 32)
|
||||
var index_data = geometry.make_quad_indices(sprites.length)
|
||||
|
||||
var vertex_count = 0
|
||||
|
||||
for (var s of sprites) {
|
||||
var px = s.pos.x != null ? s.pos.x : (s.pos[0] || 0)
|
||||
var py = s.pos.y != null ? s.pos.y : (s.pos[1] || 0)
|
||||
var white = {r: 1, g: 1, b: 1, a: 1}
|
||||
|
||||
array.for(sprites, s => {
|
||||
var px = s.pos.x
|
||||
var py = s.pos.y
|
||||
var w = s.width || 1
|
||||
var h = s.height || 1
|
||||
var ax = s.anchor_x || 0
|
||||
var ay = s.anchor_y || 0
|
||||
var c = s.color || {r: 1, g: 1, b: 1, a: 1}
|
||||
var c = s.color || white
|
||||
|
||||
// Apply anchor
|
||||
var x = px - w * ax
|
||||
@@ -810,57 +813,10 @@ function _build_sprite_vertices(sprites, camera) {
|
||||
var u1 = s.uv_rect ? (s.uv_rect.x + s.uv_rect.width) : 1
|
||||
var v1 = s.uv_rect ? (s.uv_rect.y + s.uv_rect.height) : 1
|
||||
|
||||
// Quad vertices (bottom-left, bottom-right, top-right, top-left)
|
||||
// v0: bottom-left
|
||||
vertex_data.wf(x)
|
||||
vertex_data.wf(y)
|
||||
vertex_data.wf(u0)
|
||||
vertex_data.wf(v1) // Flip V
|
||||
vertex_data.wf(c.r)
|
||||
vertex_data.wf(c.g)
|
||||
vertex_data.wf(c.b)
|
||||
vertex_data.wf(c.a)
|
||||
|
||||
// v1: bottom-right
|
||||
vertex_data.wf(x + w)
|
||||
vertex_data.wf(y)
|
||||
vertex_data.wf(u1)
|
||||
vertex_data.wf(v1) // Flip V
|
||||
vertex_data.wf(c.r)
|
||||
vertex_data.wf(c.g)
|
||||
vertex_data.wf(c.b)
|
||||
vertex_data.wf(c.a)
|
||||
|
||||
// v2: top-right
|
||||
vertex_data.wf(x + w)
|
||||
vertex_data.wf(y + h)
|
||||
vertex_data.wf(u1)
|
||||
vertex_data.wf(v0) // Flip V
|
||||
vertex_data.wf(c.r)
|
||||
vertex_data.wf(c.g)
|
||||
vertex_data.wf(c.b)
|
||||
vertex_data.wf(c.a)
|
||||
|
||||
// v3: top-left
|
||||
vertex_data.wf(x)
|
||||
vertex_data.wf(y + h)
|
||||
vertex_data.wf(u0)
|
||||
vertex_data.wf(v0) // Flip V
|
||||
vertex_data.wf(c.r)
|
||||
vertex_data.wf(c.g)
|
||||
vertex_data.wf(c.b)
|
||||
vertex_data.wf(c.a)
|
||||
|
||||
// Indices (two triangles)
|
||||
index_data.w16(vertex_count + 0)
|
||||
index_data.w16(vertex_count + 1)
|
||||
index_data.w16(vertex_count + 2)
|
||||
index_data.w16(vertex_count + 0)
|
||||
index_data.w16(vertex_count + 2)
|
||||
index_data.w16(vertex_count + 3)
|
||||
|
||||
vertex_count += 4
|
||||
}
|
||||
// call to implement
|
||||
var verts = geometry.sprite_vertices({x,y,w,h,u0,v0,u1,v1,c})
|
||||
vertex_data.write_blob(verts)
|
||||
})
|
||||
|
||||
return {
|
||||
vertices: stone(vertex_data),
|
||||
@@ -1042,6 +998,7 @@ function _execute_commands(commands, window_size) {
|
||||
var current_target = null
|
||||
var current_camera = null
|
||||
var pending_draws = []
|
||||
var target
|
||||
|
||||
// Cache swapchain texture for the duration of this command buffer
|
||||
var _swapchain_tex = null
|
||||
@@ -1067,7 +1024,7 @@ function _execute_commands(commands, window_size) {
|
||||
}
|
||||
|
||||
// Start new pass
|
||||
var target = cmd.target
|
||||
target = cmd.target
|
||||
var clear = cmd.clear
|
||||
|
||||
if (target == 'screen') {
|
||||
@@ -1209,7 +1166,7 @@ function _execute_commands(commands, window_size) {
|
||||
imgui_mod.prepare(cmd_buffer)
|
||||
|
||||
// Restart pass to the same target for rendering
|
||||
var target = cmd.target
|
||||
target = cmd.target
|
||||
var swap_tex = null
|
||||
if (target == 'screen') {
|
||||
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 = {
|
||||
pos: {x:0, y:0},
|
||||
type: 'sprite',
|
||||
pos: null,
|
||||
layer: 0,
|
||||
image: null,
|
||||
width: 1,
|
||||
@@ -9,10 +13,95 @@ var sprite = {
|
||||
anchor_y: 0,
|
||||
scale_x: 1,
|
||||
scale_y: 1,
|
||||
color: {r:1, g:1, b:1, a:1},
|
||||
animation: null, // 'walk', 'attack', etc
|
||||
color: null,
|
||||
uv_rect: null,
|
||||
slice: null,
|
||||
tile: null,
|
||||
material: null,
|
||||
animation: null,
|
||||
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