From 3d4f4ad607c756e430d58012da7eef82f7971617 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:34:53 -0400 Subject: [PATCH] Spritesheet Block, Tint Block, and Fireworks updates (#52) Co-authored-by: AmoebaChant <10319625+AmoebaChant@users.noreply.github.com> --- .../src/configuration/blockDeserializers.ts | 12 ++ .../src/configuration/blockSerializers.ts | 2 + .../src/configuration/blocks/blockNames.ts | 2 + .../effects/spritesheetBlock.fragment.glsl | 23 ++++ .../blocks/effects/spritesheetBlock.ts | 123 ++++++++++++++++++ .../blocks/effects/tintBlock.fragment.glsl | 9 ++ .../configuration/blocks/effects/tintBlock.ts | 100 ++++++++++++++ .../generators/fireworksBlock.fragment.glsl | 10 +- .../blocks/generators/fireworksBlock.ts | 34 ++++- .../editor/blockEditorRegistrations.ts | 14 ++ 10 files changed, 322 insertions(+), 7 deletions(-) create mode 100644 packages/demo/src/configuration/blocks/effects/spritesheetBlock.fragment.glsl create mode 100644 packages/demo/src/configuration/blocks/effects/spritesheetBlock.ts create mode 100644 packages/demo/src/configuration/blocks/effects/tintBlock.fragment.glsl create mode 100644 packages/demo/src/configuration/blocks/effects/tintBlock.ts diff --git a/packages/demo/src/configuration/blockDeserializers.ts b/packages/demo/src/configuration/blockDeserializers.ts index c901728a..be193790 100644 --- a/packages/demo/src/configuration/blockDeserializers.ts +++ b/packages/demo/src/configuration/blockDeserializers.ts @@ -183,6 +183,18 @@ export function getBlockDeserializers(): Map { return new NeonHeartBlock(smartFilter, serializedBlock.name); }); + deserializers.set(BlockNames.spritesheet, async (smartFilter: SmartFilter, serializedBlock: ISerializedBlockV1) => { + const { SpritesheetBlock } = await import( + /* webpackChunkName: "spritesheetBlock" */ "./blocks/effects/spritesheetBlock" + ); + return new SpritesheetBlock(smartFilter, serializedBlock.name); + }); + + deserializers.set(BlockNames.tint, async (smartFilter: SmartFilter, serializedBlock: ISerializedBlockV1) => { + const { TintBlock } = await import(/* webpackChunkName: "tintBlock" */ "./blocks/effects/tintBlock"); + return new TintBlock(smartFilter, serializedBlock.name); + }); + // Non-trivial deserializers begin. deserializers.set(BlockNames.blur, async (smartFilter: SmartFilter, serializedBlock: ISerializedBlockV1) => { diff --git a/packages/demo/src/configuration/blockSerializers.ts b/packages/demo/src/configuration/blockSerializers.ts index 48958203..04da4525 100644 --- a/packages/demo/src/configuration/blockSerializers.ts +++ b/packages/demo/src/configuration/blockSerializers.ts @@ -35,6 +35,8 @@ export const blocksUsingDefaultSerialization: string[] = [ BlockNames.particle, BlockNames.hearts, BlockNames.neonHeart, + BlockNames.spritesheet, + BlockNames.tint, ]; /** diff --git a/packages/demo/src/configuration/blocks/blockNames.ts b/packages/demo/src/configuration/blocks/blockNames.ts index 67be5e0c..046787e3 100644 --- a/packages/demo/src/configuration/blocks/blockNames.ts +++ b/packages/demo/src/configuration/blocks/blockNames.ts @@ -27,4 +27,6 @@ export const BlockNames = { particle: "ParticleBlock", hearts: "HeartsBlock", neonHeart: "NeonHeartBlock", + spritesheet: "SpritesheetBlock", + tint: "TintBlock", }; diff --git a/packages/demo/src/configuration/blocks/effects/spritesheetBlock.fragment.glsl b/packages/demo/src/configuration/blocks/effects/spritesheetBlock.fragment.glsl new file mode 100644 index 00000000..99c550e4 --- /dev/null +++ b/packages/demo/src/configuration/blocks/effects/spritesheetBlock.fragment.glsl @@ -0,0 +1,23 @@ +uniform sampler2D input; // main +uniform float time; +uniform float rows; +uniform float cols; +uniform float frames; + +vec4 mainImage(vec2 vUV) { // main + float invRows = 1.0 / rows; + float invCols = 1.0 / cols; + + // Get offset of frame + float frame = mod(floor(time), frames); + float row = (rows - 1.0) - floor(frame * invCols); // Reverse row direction b/c UVs start from bottom + float col = mod(frame, cols); + + // Add offset, then scale UV down to frame size + vUV = vec2( + (vUV.x + col) * invCols, + (vUV.y + row) * invRows + ); + + return texture2D(input, vUV); +} \ No newline at end of file diff --git a/packages/demo/src/configuration/blocks/effects/spritesheetBlock.ts b/packages/demo/src/configuration/blocks/effects/spritesheetBlock.ts new file mode 100644 index 00000000..3642b78c --- /dev/null +++ b/packages/demo/src/configuration/blocks/effects/spritesheetBlock.ts @@ -0,0 +1,123 @@ +import type { Effect } from "@babylonjs/core/Materials/effect"; +import { type SmartFilter, type IDisableableBlock, type RuntimeData, createStrongRef } from "@babylonjs/smart-filters"; +import { ShaderBlock, ConnectionPointType, ShaderBinding } from "@babylonjs/smart-filters"; +import { BlockNames } from "../blockNames"; +import { shaderProgram, uniforms } from "./spritesheetBlock.shader"; + +/** + * The shader bindings for the Spritesheet block. + */ +export class SpritesheetShaderBinding extends ShaderBinding { + private readonly _inputTexture: RuntimeData; + private readonly _time: RuntimeData; + private readonly _rows: RuntimeData; + private readonly _cols: RuntimeData; + private readonly _frames: RuntimeData; + + /** + * Creates a new shader binding instance for the SpriteSheet block. + * @param parentBlock - The parent block + * @param inputTexture - The input texture + * @param time - The time passed since the start of the effect + * @param rows - The number of rows in the sprite sheet + * @param cols - The number of columns in the sprite sheet + * @param frames - The number of frames to show + */ + constructor( + parentBlock: IDisableableBlock, + inputTexture: RuntimeData, + time: RuntimeData, + rows: RuntimeData, + cols: RuntimeData, + frames: RuntimeData + ) { + super(parentBlock); + this._inputTexture = inputTexture; + this._time = time; + this._rows = rows; + this._cols = cols; + this._frames = frames; + } + + /** + * Binds all the required data to the shader when rendering. + * @param effect - defines the effect to bind the data to + */ + public override bind(effect: Effect): void { + super.bind(effect); + effect.setTexture(this.getRemappedName(uniforms.input), this._inputTexture.value); + effect.setFloat(this.getRemappedName(uniforms.time), this._time.value); + effect.setFloat(this.getRemappedName(uniforms.rows), this._rows.value); + effect.setFloat(this.getRemappedName(uniforms.cols), this._cols.value); + + // Apply default value for frame count if it was not provided + effect.setFloat( + this.getRemappedName(uniforms.frames), + this._frames.value > 0 ? this._frames.value : this._rows.value * this._cols.value + ); + } +} + +/** + * A block that animates a sprite sheet texture. + */ +export class SpritesheetBlock extends ShaderBlock { + /** + * The class name of the block. + */ + public static override ClassName = BlockNames.spritesheet; + + /** + * The input texture connection point + */ + public readonly input = this._registerInput("input", ConnectionPointType.Texture); + + /** + * The time connection point to animate the effect. + */ + public readonly time = this._registerOptionalInput("time", ConnectionPointType.Float, createStrongRef(0.0)); + + /** + * The number of rows in the sprite sheet, as a connection point. + */ + public readonly rows = this._registerOptionalInput("rows", ConnectionPointType.Float, createStrongRef(1.0)); + + /** + * The number of columns in the sprite sheet, as a connection point. + */ + public readonly columns = this._registerOptionalInput("columns", ConnectionPointType.Float, createStrongRef(1.0)); + + /** + * The number of frames to animate from the beginning, as a connection point. + * Defaults to rows * columns at runtime. + */ + public readonly frames = this._registerOptionalInput("frames", ConnectionPointType.Float, createStrongRef(0.0)); + + /** + * The shader program (vertex and fragment code) to use to render the block + */ + public static override ShaderCode = shaderProgram; + + /** + * Instantiates a new Block. + * @param smartFilter - The smart filter this block belongs to + * @param name - The friendly name of the block + */ + constructor(smartFilter: SmartFilter, name: string) { + super(smartFilter, name); + } + + /** + * Get the class instance that binds all the required data to the shader (effect) when rendering. + * @returns The class instance that binds the data to the effect + */ + public getShaderBinding(): ShaderBinding { + const input = this._confirmRuntimeDataSupplied(this.input); + const rows = this.rows.runtimeData; + const columns = this.columns.runtimeData; + const time = this.time.runtimeData; + const frames = this.frames.runtimeData; + + return new SpritesheetShaderBinding(this, input, time, rows, columns, frames); + } +} diff --git a/packages/demo/src/configuration/blocks/effects/tintBlock.fragment.glsl b/packages/demo/src/configuration/blocks/effects/tintBlock.fragment.glsl new file mode 100644 index 00000000..abf9db08 --- /dev/null +++ b/packages/demo/src/configuration/blocks/effects/tintBlock.fragment.glsl @@ -0,0 +1,9 @@ +uniform sampler2D input; // main +uniform vec3 tint; +uniform float amount; + +vec4 mainImage(vec2 vUV) { // main + vec4 color = texture2D(input, vUV); + vec3 tinted = mix(color.rgb, tint, amount); + return vec4(tinted, color.a); +} \ No newline at end of file diff --git a/packages/demo/src/configuration/blocks/effects/tintBlock.ts b/packages/demo/src/configuration/blocks/effects/tintBlock.ts new file mode 100644 index 00000000..0f031434 --- /dev/null +++ b/packages/demo/src/configuration/blocks/effects/tintBlock.ts @@ -0,0 +1,100 @@ +import type { Effect } from "@babylonjs/core/Materials/effect"; +import type { SmartFilter, IDisableableBlock, RuntimeData } from "@babylonjs/smart-filters"; +import { ShaderBlock, ConnectionPointType, ShaderBinding, createStrongRef } from "@babylonjs/smart-filters"; +import { BlockNames } from "../blockNames"; +import { uniforms, shaderProgram } from "./tintBlock.shader"; +import { Color3 } from "@babylonjs/core/Maths/math.color"; + +/** + * The shader bindings for the Tint block. + */ +export class TintShaderBinding extends ShaderBinding { + private readonly _inputTexture: RuntimeData; + private readonly _tint: RuntimeData; + private readonly _amount: RuntimeData; + + /** + * Creates a new shader binding instance for the Tint block. + * @param parentBlock - The parent block + * @param inputTexture - the input texture + * @param tint - the tint to apply + * @param amount - the amount of tint to apply + */ + constructor( + parentBlock: IDisableableBlock, + inputTexture: RuntimeData, + tint: RuntimeData, + amount: RuntimeData + ) { + super(parentBlock); + this._inputTexture = inputTexture; + this._tint = tint; + this._amount = amount; + } + + /** + * Binds all the required data to the shader when rendering. + * @param effect - defines the effect to bind the data to + */ + public override bind(effect: Effect): void { + super.bind(effect); + effect.setTexture(this.getRemappedName(uniforms.input), this._inputTexture.value); + effect.setColor3(this.getRemappedName(uniforms.tint), this._tint.value); + effect.setFloat(this.getRemappedName(uniforms.amount), this._amount.value); + } +} + +/** + * A simple block to apply a tint to a texture + */ +export class TintBlock extends ShaderBlock { + /** + * The class name of the block. + */ + public static override ClassName = BlockNames.tint; + + /** + * The input texture connection point. + */ + public readonly input = this._registerInput("input", ConnectionPointType.Texture); + + /** + * The tint color connection point. + */ + public readonly tint = this._registerOptionalInput( + "tint", + ConnectionPointType.Color3, + createStrongRef(new Color3(1, 0, 0)) + ); + + /** + * The strength of the tint, as a connection point. + */ + public readonly amount = this._registerOptionalInput("amount", ConnectionPointType.Float, createStrongRef(0.25)); + + /** + * The shader program (vertex and fragment code) to use to render the block + */ + public static override ShaderCode = shaderProgram; + + /** + * Instantiates a new Block. + * @param smartFilter - The smart filter this block belongs to + * @param name - The friendly name of the block + */ + constructor(smartFilter: SmartFilter, name: string) { + super(smartFilter, name); + } + + /** + * Get the class instance that binds all the required data to the shader (effect) when rendering. + * @returns The class instance that binds the data to the effect + */ + public getShaderBinding(): ShaderBinding { + const input = this._confirmRuntimeDataSupplied(this.input); + const tint = this.tint.runtimeData; + const amount = this.amount.runtimeData; + + return new TintShaderBinding(this, input, tint, amount); + } +} diff --git a/packages/demo/src/configuration/blocks/generators/fireworksBlock.fragment.glsl b/packages/demo/src/configuration/blocks/generators/fireworksBlock.fragment.glsl index 44a41643..7f6cf9c5 100644 --- a/packages/demo/src/configuration/blocks/generators/fireworksBlock.fragment.glsl +++ b/packages/demo/src/configuration/blocks/generators/fireworksBlock.fragment.glsl @@ -7,10 +7,10 @@ uniform sampler2D input; // main uniform float time; uniform float aspectRatio; +uniform float fireworks; +uniform float fireworkSparks; const float PI = 3.141592653589793; -const float EXPLOSION_COUNT = 8.; -const float SPARKS_PER_EXPLOSION = 128.; const float EXPLOSION_DURATION = 20.; const float EXPLOSION_SPEED = 5.; const float EXPLOSION_RADIUS_THESHOLD = .06; @@ -29,14 +29,14 @@ vec4 mainImage(vec2 vUV) { // main vec2 origin = vec2(0.); vUV.x *= aspectRatio; - for (float j = 0.; j < EXPLOSION_COUNT; ++j) + for (float j = 0.; j < fireworks; ++j) { - vec3 oh = hash31((j + 1234.1939) * 641.6974); + vec3 oh = hash31((j + 800.) * 641.6974); origin = vec2(oh.x, oh.y) * .6 + .2; // .2 - .8 to avoid boundaries origin.x *= aspectRatio; // Change t value to randomize the spawning of explosions t += (j + 1.) * 9.6491 * oh.z; - for (float i = 0.; i < SPARKS_PER_EXPLOSION; ++i) + for (float i = 0.; i < fireworkSparks; ++i) { vec3 h = hash31(j * 963.31 + i + 497.8943); // random angle (0 - 2*PI) diff --git a/packages/demo/src/configuration/blocks/generators/fireworksBlock.ts b/packages/demo/src/configuration/blocks/generators/fireworksBlock.ts index 34390d40..08151cfa 100644 --- a/packages/demo/src/configuration/blocks/generators/fireworksBlock.ts +++ b/packages/demo/src/configuration/blocks/generators/fireworksBlock.ts @@ -11,21 +11,29 @@ import { shaderProgram, uniforms } from "../generators/fireworksBlock.shader"; export class FireworksShaderBinding extends ShaderBinding { private readonly _inputTexture: RuntimeData; private readonly _time: RuntimeData; + private readonly _fireworks: RuntimeData; + private readonly _fireworkSparks: RuntimeData; /** * Creates a new shader binding instance for the Fireworks block. * @param parentBlock - The parent block * @param inputTexture - The input texture * @param time - The time passed since the start of the effect + * @param fireworks - The number of fireworks to display + * @param fireworkSparks - The number of sparks per firework */ constructor( parentBlock: IDisableableBlock, inputTexture: RuntimeData, - time: RuntimeData + time: RuntimeData, + fireworks: RuntimeData, + fireworkSparks: RuntimeData ) { super(parentBlock); this._inputTexture = inputTexture; this._time = time; + this._fireworks = fireworks; + this._fireworkSparks = fireworkSparks; } /** @@ -39,6 +47,8 @@ export class FireworksShaderBinding extends ShaderBinding { effect.setTexture(this.getRemappedName(uniforms.input), this._inputTexture.value); effect.setFloat(this.getRemappedName(uniforms.aspectRatio), _width / _height); effect.setFloat(this.getRemappedName(uniforms.time), this._time.value); + effect.setFloat(this.getRemappedName(uniforms.fireworks), this._fireworks.value); + effect.setFloat(this.getRemappedName(uniforms.fireworkSparks), this._fireworkSparks.value); } } @@ -61,6 +71,24 @@ export class FireworksBlock extends ShaderBlock { */ public readonly time = this._registerOptionalInput("time", ConnectionPointType.Float, createStrongRef(0.3)); + /** + * The number of fireworks at a time, as a connection point. + */ + public readonly fireworks = this._registerOptionalInput( + "fireworks", + ConnectionPointType.Float, + createStrongRef(8.0) + ); + + /** + * The number of sparks per firework, as a connection point. + */ + public readonly fireworkSparks = this._registerOptionalInput( + "fireworkSparks", + ConnectionPointType.Float, + createStrongRef(128.0) + ); + /** * The shader program (vertex and fragment code) to use to render the block */ @@ -82,7 +110,9 @@ export class FireworksBlock extends ShaderBlock { public getShaderBinding(): ShaderBinding { const input = this._confirmRuntimeDataSupplied(this.input); const time = this.time.runtimeData; + const fireworks = this.fireworks.runtimeData; + const fireworkSparks = this.fireworkSparks.runtimeData; - return new FireworksShaderBinding(this, input, time); + return new FireworksShaderBinding(this, input, time, fireworks, fireworkSparks); } } diff --git a/packages/demo/src/configuration/editor/blockEditorRegistrations.ts b/packages/demo/src/configuration/editor/blockEditorRegistrations.ts index 71feb2b1..597457a9 100644 --- a/packages/demo/src/configuration/editor/blockEditorRegistrations.ts +++ b/packages/demo/src/configuration/editor/blockEditorRegistrations.ts @@ -30,6 +30,8 @@ import { WebCamInputBlockName } from "../blocks/inputs/webCamInputBlock"; import { ParticleBlock } from "../blocks/generators/particleBlock"; import { HeartsBlock } from "../blocks/generators/heartsBlock"; import { NeonHeartBlock } from "../blocks/generators/neonHeartBlock"; +import { SpritesheetBlock } from "../blocks/effects/spritesheetBlock"; +import { TintBlock } from "../blocks/effects/tintBlock"; export const blockEditorRegistrations: IBlockEditorRegistration[] = [ ...defaultBlockEditorRegistrations, @@ -268,4 +270,16 @@ export const blockEditorRegistrations: IBlockEditorRegistration[] = [ category: "Generators", tooltip: "A drawn, neon heart at the center of the input texture", }, + { + name: "SpritesheetBlock", + factory: (smartFilter: SmartFilter) => new SpritesheetBlock(smartFilter, "Spritesheet"), + category: "Effects", + tooltip: "Animates a sprite sheet texture", + }, + { + name: "TintBlock", + factory: (smartFilter: SmartFilter) => new TintBlock(smartFilter, "Tint"), + category: "Effects", + tooltip: "Adds colored tint to the input texture", + }, ];