From 8e442ad4e3faee4b69460331f0e0c72f015052c3 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Wed, 14 May 2025 11:23:58 +0200 Subject: [PATCH 1/2] added timing functionality to shaders --- examples/tests/shader-time.ts | 141 ++++++++++++++++++ src/core/CoreNode.ts | 25 +++- src/core/Stage.ts | 16 +- src/core/renderers/CoreRenderer.ts | 1 + src/core/renderers/CoreShaderNode.ts | 7 + src/core/renderers/webgl/WebGlRenderOp.ts | 5 +- .../renderers/webgl/WebGlShaderProgram.ts | 16 ++ 7 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 examples/tests/shader-time.ts diff --git a/examples/tests/shader-time.ts b/examples/tests/shader-time.ts new file mode 100644 index 00000000..61202309 --- /dev/null +++ b/examples/tests/shader-time.ts @@ -0,0 +1,141 @@ +import type { WebGlShaderType } from '../../dist/exports/webgl-shaders.js'; +import type { ExampleSettings } from '../common/ExampleSettings.js'; + +export default async function ({ renderer, testRoot }: ExampleSettings) { + renderer.stage.shManager.registerShaderType('Spinner', Spinner); + + renderer.createNode({ + x: 90, + y: 90, + width: 90, + height: 90, + color: 0xff0000ff, + shader: renderer.createShader('Spinner'), + parent: testRoot, + }); + + renderer.createNode({ + x: 290, + y: 90, + width: 90, + height: 90, + color: 0xff0000ff, + shader: renderer.createShader('Spinner', { + clockwise: false, + }), + parent: testRoot, + }); + + renderer.createNode({ + x: 490, + y: 90, + width: 90, + height: 90, + color: 0xff0000ff, + shader: renderer.createShader('Spinner', { + period: 0.4, + }), + parent: testRoot, + }); +} + +export const Spinner: WebGlShaderType = { + props: { + clockwise: true, + period: 1, + }, + update() { + this.uniform1f('u_clockwise', this.props!.clockwise === true ? 1 : -1); + this.uniform1f('u_period', this.props!.period as number); + }, + time: true, + vertex: ` + # ifdef GL_FRAGMENT_PRECISION_HIGH + precision highp float; + # else + precision mediump float; + # endif + + attribute vec2 a_position; + attribute vec2 a_textureCoords; + attribute vec4 a_color; + attribute vec2 a_nodeCoords; + + uniform vec2 u_resolution; + uniform float u_pixelRatio; + uniform vec2 u_dimensions; + uniform float u_time; + + uniform float u_period; + + varying vec4 v_color; + varying vec2 v_textureCoords; + varying vec2 v_nodeCoords; + + varying vec2 v_innerSize; + varying vec2 v_halfDimensions; + varying float v_time; + + void main() { + vec2 normalized = a_position * u_pixelRatio; + vec2 screenSpace = vec2(2.0 / u_resolution.x, -2.0 / u_resolution.y); + + v_color = a_color; + v_nodeCoords = a_nodeCoords; + v_textureCoords = a_textureCoords; + + v_halfDimensions = u_dimensions * 0.5; + + v_time = u_time / 1000.0 / u_period; + + gl_Position = vec4(normalized.x * screenSpace.x - 1.0, normalized.y * -abs(screenSpace.y) + 1.0, 0.0, 1.0); + gl_Position.y = -sign(screenSpace.y) * gl_Position.y; + } + `, + fragment: ` + # ifdef GL_FRAGMENT_PRECISION_HIGH + precision highp float; + # else + precision mediump float; + # endif + + #define PI 3.14159265359 + + uniform vec2 u_resolution; + uniform float u_pixelRatio; + uniform float u_alpha; + uniform vec2 u_dimensions; + uniform sampler2D u_texture; + + uniform float u_clockwise; + + varying vec4 v_color; + varying vec2 v_nodeCoords; + varying vec2 v_textureCoords; + + varying vec2 v_halfDimensions; + varying float v_time; + + float circleDist(vec2 p, float radius){ + return length(p) - radius; + } + + float fillMask(float dist){ + return clamp(-dist, 0.0, 1.0); + } + + void main() { + vec4 color = texture2D(u_texture, v_textureCoords) * v_color; + vec2 uv = v_nodeCoords.xy * u_dimensions - v_halfDimensions; + + float c = max(-circleDist(uv, v_halfDimensions.x - 10.0), circleDist(uv, v_halfDimensions.x)); + float r = -v_time * 6.0 * u_clockwise; + + uv *= mat2(cos(r), sin(r), -sin(r), cos(r)); + + float a = u_clockwise * atan(uv.x, uv.y) * PI * 0.05 + 0.45; + + gl_FragColor = mix(vec4(0.0), color, fillMask(c) * a); + } + `, +}; diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 0b1cef0b..fbb19d68 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -21,7 +21,6 @@ import { assertTruthy, getNewId, mergeColorAlphaPremultiplied, - isProductionEnvironment, } from '../utils.js'; import type { TextureOptions } from './CoreTextureManager.js'; import type { CoreRenderer } from './renderers/CoreRenderer.js'; @@ -729,6 +728,7 @@ export class CoreNode extends EventEmitter { readonly props: CoreNodeProps; private hasShaderUpdater = false; + public hasShaderTimeFn = false; public updateType = UpdateType.All; public childUpdateType = UpdateType.None; @@ -1737,7 +1737,7 @@ export class CoreNode extends EventEmitter { } } - assertTruthy(this.globalTransform); + const globalTransform = this.globalTransform!; // add to list of renderables to be sorted before rendering renderer.addQuad({ @@ -1756,12 +1756,12 @@ export class CoreNode extends EventEmitter { shader: this.props.shader as CoreShaderNode, alpha: this.worldAlpha, clippingRect: this.clippingRect, - tx: this.globalTransform.tx, - ty: this.globalTransform.ty, - ta: this.globalTransform.ta, - tb: this.globalTransform.tb, - tc: this.globalTransform.tc, - td: this.globalTransform.td, + tx: globalTransform.tx, + ty: globalTransform.ty, + ta: globalTransform.ta, + tb: globalTransform.tb, + tc: globalTransform.tc, + td: globalTransform.td, renderCoords: this.renderCoords, rtt: this.rtt, parentHasRenderTexture: this.parentHasRenderTexture, @@ -1769,9 +1769,17 @@ export class CoreNode extends EventEmitter { this.parentHasRenderTexture === true ? this.parentFramebufferDimensions : null, + time: this.hasShaderTimeFn === true ? this.getTimerValue() : null, }); } + getTimerValue(): number { + if (typeof this.shader!.time === 'function') { + return this.shader!.time(this.stage); + } + return this.stage.elapsedTime; + } + //#region Properties get id(): number { return this._id; @@ -2306,6 +2314,7 @@ export class CoreNode extends EventEmitter { } if (shader.shaderKey !== 'default') { this.hasShaderUpdater = shader.update !== undefined; + this.hasShaderTimeFn = shader.time !== undefined; shader.attachNode(this); } this.props.shader = shader; diff --git a/src/core/Stage.ts b/src/core/Stage.ts index f962e8bb..353414f2 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -121,9 +121,12 @@ export class Stage { public readonly eventBus: EventEmitter; /// State + startTime = 0; deltaTime = 0; lastFrameTime = 0; currentFrameTime = 0; + elapsedTime = 0; + private timedNodes = 0; private clrColor = 0x00000000; private fpsNumFrames = 0; private fpsElapsedTime = 0; @@ -163,6 +166,8 @@ export class Stage { this.platform = platform; + this.startTime = platform.getTimeStamp(); + this.eventBus = options.eventBus; this.txManager = new CoreTextureManager(this, { numImageWorkers, @@ -302,9 +307,10 @@ export class Stage { } updateFrameTime() { - const newFrameTime = this.platform!.getTimeStamp(); + const newFrameTime = this.platform.getTimeStamp(); this.lastFrameTime = this.currentFrameTime; this.currentFrameTime = newFrameTime; + this.elapsedTime = this.startTime - newFrameTime; this.deltaTime = !this.lastFrameTime ? 100 / 6 : newFrameTime - this.lastFrameTime; @@ -415,6 +421,11 @@ export class Stage { if (renderRequested) { this.renderRequested = false; } + + if (this.timedNodes > 0) { + this.timedNodes = 0; + this.requestRender(); + } } /** @@ -489,6 +500,9 @@ export class Stage { // If the node is renderable and has a loaded texture, render it if (node.isRenderable === true) { node.renderQuads(this.renderer); + if (node.hasShaderTimeFn === true) { + this.timedNodes++; + } } for (let i = 0; i < node.children.length; i++) { diff --git a/src/core/renderers/CoreRenderer.ts b/src/core/renderers/CoreRenderer.ts index b438519d..050f3b53 100644 --- a/src/core/renderers/CoreRenderer.ts +++ b/src/core/renderers/CoreRenderer.ts @@ -53,6 +53,7 @@ export interface QuadOptions { rtt: boolean; parentHasRenderTexture: boolean; framebufferDimensions: Dimensions | null; + time?: number | null; } export interface CoreRendererOptions { diff --git a/src/core/renderers/CoreShaderNode.ts b/src/core/renderers/CoreShaderNode.ts index bf01c508..1e94fa2a 100644 --- a/src/core/renderers/CoreShaderNode.ts +++ b/src/core/renderers/CoreShaderNode.ts @@ -75,6 +75,11 @@ export interface CoreShaderType { * used for making a cache key to check for reusability, currently only used for webgl ShaderTypes but might be needed for other types of renderer */ getCacheMarkers?: (props: Props) => string; + /** + * timer that updates every loop, by default uses the stage elapsed time If you want to do a special calculation you can define a function. + * When you calculate your own value you can use the Stage timing values deltaTime, lastFrameTime, and currentFrameTime; + */ + time?: boolean | ((stage: Stage) => number); } /** @@ -89,6 +94,7 @@ export class CoreShaderNode> { readonly resolvedProps: Props | undefined = undefined; protected definedProps: Props | undefined = undefined; protected node: CoreNode | null = null; + readonly time: CoreShaderType['time'] = undefined; update: (() => void) | undefined = undefined; constructor( @@ -99,6 +105,7 @@ export class CoreShaderNode> { ) { this.stage = stage; this.shaderType = type; + this.time = type.time; if (props !== undefined) { /** diff --git a/src/core/renderers/webgl/WebGlRenderOp.ts b/src/core/renderers/webgl/WebGlRenderOp.ts index de70c995..42700b32 100644 --- a/src/core/renderers/webgl/WebGlRenderOp.ts +++ b/src/core/renderers/webgl/WebGlRenderOp.ts @@ -34,7 +34,8 @@ type ReqQuad = | 'rtt' | 'clippingRect' | 'height' - | 'width'; + | 'width' + | 'time'; type RenderOpQuadOptions = Pick & Partial> & { sdfShaderProps?: Record; @@ -65,6 +66,7 @@ export class WebGlRenderOp extends CoreRenderOp { readonly framebufferDimensions?: Dimensions | null; readonly alpha: number; readonly pixelRatio: number; + readonly time?: number | null; constructor( readonly renderer: WebGlRenderer, @@ -83,6 +85,7 @@ export class WebGlRenderOp extends CoreRenderOp { this.alpha = quad.alpha; this.pixelRatio = this.parentHasRenderTexture === true ? 1 : renderer.stage.pixelRatio; + this.time = quad.time; /** * related to line 51 diff --git a/src/core/renderers/webgl/WebGlShaderProgram.ts b/src/core/renderers/webgl/WebGlShaderProgram.ts index 921d01ee..9cf135f1 100644 --- a/src/core/renderers/webgl/WebGlShaderProgram.ts +++ b/src/core/renderers/webgl/WebGlShaderProgram.ts @@ -50,6 +50,7 @@ export class WebGlShaderProgram implements CoreShaderProgram { protected lifecycle: Pick; protected useSystemAlpha = false; protected useSystemDimensions = false; + protected useTimeValue = false; supportsIndexedTextures = false; constructor( @@ -122,6 +123,10 @@ export class WebGlShaderProgram implements CoreShaderProgram { this.useSystemDimensions = this.glw.getUniformLocation(program, 'u_dimensions') !== null; + this.useTimeValue = + this.glw.getUniformLocation(program, 'u_dimensions') !== null && + config.time !== undefined; + this.lifecycle = { update: config.update, canBatch: config.canBatch, @@ -149,6 +154,12 @@ export class WebGlShaderProgram implements CoreShaderProgram { return this.lifecycle.canBatch(incomingQuad, currentRenderOp); } + if (this.useTimeValue === true) { + if (incomingQuad.time !== currentRenderOp.time) { + return false; + } + } + if (this.useSystemAlpha === true) { if (incomingQuad.alpha !== currentRenderOp.alpha) { return false; @@ -169,6 +180,7 @@ export class WebGlShaderProgram implements CoreShaderProgram { if (incomingQuad.shader !== null) { shaderPropsA = incomingQuad.shader.resolvedProps; } + if (currentRenderOp.shader !== null) { shaderPropsB = currentRenderOp.shader.resolvedProps; } @@ -221,6 +233,10 @@ export class WebGlShaderProgram implements CoreShaderProgram { ); } + if (this.useTimeValue === true) { + this.glw.uniform1f('u_time', renderOp.time as number); + } + // if (this.useSystemAlpha) { this.glw.uniform1f('u_alpha', renderOp.alpha); // } From 8c233853fbf8e8dde51b8695f059319c915b00ab Mon Sep 17 00:00:00 2001 From: jfboeve Date: Fri, 15 Aug 2025 15:42:11 +0200 Subject: [PATCH 2/2] improved how timedNodes are tracked --- examples/tests/shader-time.ts | 12 +++++------ src/core/CoreNode.ts | 11 +++++++++- src/core/Stage.ts | 39 ++++++++++++++++++++++++++++------- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/examples/tests/shader-time.ts b/examples/tests/shader-time.ts index 61202309..8bb8f4d9 100644 --- a/examples/tests/shader-time.ts +++ b/examples/tests/shader-time.ts @@ -7,8 +7,8 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { renderer.createNode({ x: 90, y: 90, - width: 90, - height: 90, + w: 90, + h: 90, color: 0xff0000ff, shader: renderer.createShader('Spinner'), parent: testRoot, @@ -17,8 +17,8 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { renderer.createNode({ x: 290, y: 90, - width: 90, - height: 90, + w: 90, + h: 90, color: 0xff0000ff, shader: renderer.createShader('Spinner', { clockwise: false, @@ -29,8 +29,8 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { renderer.createNode({ x: 490, y: 90, - width: 90, - height: 90, + w: 90, + h: 90, color: 0xff0000ff, shader: renderer.createShader('Spinner', { period: 0.4, diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index c5ac0a3a..f93fd6f7 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -1672,6 +1672,9 @@ export class CoreNode extends EventEmitter { this.destroyed = true; this.unloadTexture(); this.isRenderable = false; + if (this.hasShaderTimeFn === true) { + this.stage.untrackTimedNode(this); + } // Kill children while (this.children.length > 0) { @@ -1733,7 +1736,7 @@ export class CoreNode extends EventEmitter { framebufferDimensions: this.parentHasRenderTexture ? this.parentFramebufferDimensions : null, - time: this.hasShaderTimeFn === true ? this.getTimerValue() : null, + time: this.hasShaderTimeFn === true ? this.getTimerValue() : null, }); } @@ -2304,6 +2307,12 @@ export class CoreNode extends EventEmitter { this.hasShaderTimeFn = shader.time !== undefined; shader.attachNode(this); } + + if (this.hasShaderTimeFn === true) { + this.stage.trackTimedNode(this); + } else { + this.stage.untrackTimedNode(this); + } this.props.shader = shader; this.setUpdateType(UpdateType.IsRenderable | UpdateType.RecalcUniforms); } diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 08b561e2..09b02e7c 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -135,7 +135,7 @@ export class Stage { lastFrameTime = 0; currentFrameTime = 0; elapsedTime = 0; - private timedNodes = 0; + private timedNodes: CoreNode[] = []; private clrColor = 0x00000000; private fpsNumFrames = 0; private fpsElapsedTime = 0; @@ -511,9 +511,13 @@ export class Stage { this.renderRequested = false; } - if (this.timedNodes > 0) { - this.timedNodes = 0; - this.requestRender(); + if (this.timedNodes.length > 0) { + for (let key in this.timedNodes) { + if (this.timedNodes[key]!.isRenderable === true) { + this.requestRender(); + break; + } + } } } @@ -589,9 +593,6 @@ export class Stage { // If the node is renderable and has a loaded texture, render it if (node.isRenderable === true) { node.renderQuads(this.renderer); - if (node.hasShaderTimeFn === true) { - this.timedNodes++; - } } for (let i = 0; i < node.children.length; i++) { @@ -780,6 +781,30 @@ export class Stage { return topNode || null; } + /** + * add node to timeNodes arrays + * @param node + * @returns + */ + trackTimedNode(node: CoreNode) { + if (this.timedNodes[node.id] !== undefined) { + return; + } + this.timedNodes[node.id] = node; + } + + /** + * remove node from timeNodes arrays + * @param node + * @returns + */ + untrackTimedNode(node: CoreNode) { + if (this.timedNodes[node.id] === undefined) { + return; + } + delete this.timedNodes[node.id]; + } + /** * Resolves the default property values for a Node *