diff --git a/examples/tests/shader-time.ts b/examples/tests/shader-time.ts new file mode 100644 index 00000000..8bb8f4d9 --- /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, + w: 90, + h: 90, + color: 0xff0000ff, + shader: renderer.createShader('Spinner'), + parent: testRoot, + }); + + renderer.createNode({ + x: 290, + y: 90, + w: 90, + h: 90, + color: 0xff0000ff, + shader: renderer.createShader('Spinner', { + clockwise: false, + }), + parent: testRoot, + }); + + renderer.createNode({ + x: 490, + y: 90, + w: 90, + h: 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 79585b63..f93fd6f7 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -721,8 +721,8 @@ export class CoreNode extends EventEmitter { readonly props: CoreNodeProps; private hasShaderUpdater = false; + public hasShaderTimeFn = false; private hasColorProps = false; - public updateType = UpdateType.All; public childUpdateType = UpdateType.None; @@ -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,9 +1736,17 @@ export class CoreNode extends EventEmitter { framebufferDimensions: this.parentHasRenderTexture ? 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; @@ -2293,8 +2304,15 @@ export class CoreNode extends EventEmitter { } if (shader.shaderKey !== 'default') { this.hasShaderUpdater = shader.update !== undefined; + 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 0ceda96e..09b02e7c 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -130,9 +130,12 @@ export class Stage { public readonly eventBus: EventEmitter; /// State + startTime = 0; deltaTime = 0; lastFrameTime = 0; currentFrameTime = 0; + elapsedTime = 0; + private timedNodes: CoreNode[] = []; private clrColor = 0x00000000; private fpsNumFrames = 0; private fpsElapsedTime = 0; @@ -177,6 +180,8 @@ export class Stage { this.platform = platform; + this.startTime = platform.getTimeStamp(); + this.eventBus = options.eventBus; // Calculate target frame time from targetFPS option @@ -386,9 +391,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; @@ -504,6 +510,15 @@ export class Stage { if (renderRequested === true) { this.renderRequested = false; } + + if (this.timedNodes.length > 0) { + for (let key in this.timedNodes) { + if (this.timedNodes[key]!.isRenderable === true) { + this.requestRender(); + break; + } + } + } } /** @@ -766,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 * 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 9a581237..08baf52e 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 4819d072..65105163 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 e53af703..6f65874c 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; public isDestroyed = false; supportsIndexedTextures = false; @@ -123,6 +124,10 @@ export class WebGlShaderProgram implements CoreShaderProgram { this.useSystemAlpha = uniLocs['u_alpha'] !== undefined; this.useSystemDimensions = uniLocs['u_dimensions'] !== undefined; + this.useTimeValue = + this.glw.getUniformLocation(program, 'u_dimensions') !== null && + config.time !== undefined; + this.lifecycle = { update: config.update, canBatch: config.canBatch, @@ -150,6 +155,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; @@ -170,6 +181,7 @@ export class WebGlShaderProgram implements CoreShaderProgram { if (incomingQuad.shader !== null) { shaderPropsA = incomingQuad.shader.resolvedProps; } + if (currentRenderOp.shader !== null) { shaderPropsB = currentRenderOp.shader.resolvedProps; } @@ -222,6 +234,10 @@ export class WebGlShaderProgram implements CoreShaderProgram { ); } + if (this.useTimeValue === true) { + this.glw.uniform1f('u_time', renderOp.time as number); + } + if (this.useSystemAlpha === true) { this.glw.uniform1f('u_alpha', renderOp.alpha); }