p5
Version:
[](https://www.npmjs.com/package/p5)
9,290 lines • 309 kB
JavaScript
/**
* @module Constants
* @submodule Constants
* @for p5
*/
const _PI = Math.PI;
/**
* Version of this p5.js.
* @property {String} VERSION
* @final
*/
const VERSION = '2.3.2';
// GRAPHICS RENDERER
/**
* The default, two-dimensional renderer in p5.js.
*
* Use this when calling <a href="#/p5/createCanvas"> (for example,
* `createCanvas(400, 400, P2D)`) to specify a 2D context.
*
* @typedef {'p2d'} P2D
* @property {P2D} P2D
* @final
*/
const P2D = 'p2d';
/**
* An expanded color space (P3) variant of the default, two-dimensional renderer.
*
* When available, this mode can allow for extended color ranges and more
* dynamic color representation. Use it similarly to `P2D`:
* `createCanvas(400, 400, P2DP3)`.
*
* @typedef {'p2d-p3'} P2DP3
* @property {P2DP3} P2DP3
* @final
*/
const P2DP3 = 'p2d-p3';
/**
* One of the two render modes in p5.js, used for computationally intensive tasks like 3D rendering and shaders.
*
* `WEBGL` differs from the default <a href="/reference/p5/P2D">`P2D`</a> renderer in the following ways:
*
* - **Coordinate System** - When drawing in `WEBGL` mode, the origin point (0,0,0) is located at the center of the screen, not the top-left corner. See <a href="https://p5js.org/tutorials/coordinates-and-transformations/">the tutorial page about coordinates and transformations</a>.
* - **3D Shapes** - `WEBGL` mode can be used to draw 3-dimensional shapes like <a href="#/p5/box">box()</a>, <a href="#/p5/sphere">sphere()</a>, <a href="#/p5/cone">cone()</a>, and <a href="https://p5js.org/reference/#3D%20Primitives">more</a>. See <a href="https://p5js.org/tutorials/custom-geometry/">the tutorial page about custom geometry</a> to make more complex objects.
* - **Shape Detail** - When drawing in `WEBGL` mode, you can specify how smooth curves should be drawn by using a `detail` parameter. See <a href="https://github.com/processing/p5.js/wiki/Getting-started-with-WebGL-in-p5#3d-primitives-shapes">the wiki section about shapes</a> for a more information and an example.
* - **Textures** - A texture is like a skin that wraps onto a shape. See <a href="https://github.com/processing/p5.js/wiki/Getting-started-with-WebGL-in-p5#textures">the wiki section about textures</a> for examples of mapping images onto surfaces with textures.
* - **Materials and Lighting** - `WEBGL` offers different types of lights like <a href="#/p5/ambientLight">ambientLight()</a> to place around a scene. Materials like <a href="#/p5/specularMaterial">specularMaterial()</a> reflect the lighting to convey shape and depth. See <a href="https://p5js.org/tutorials/lights-camera-materials/">the tutorial page for styling and appearance</a> to experiment with different combinations.
* - **Camera** - The viewport of a `WEBGL` sketch can be adjusted by changing camera attributes. See <a href="https://p5js.org/tutorials/lights-camera-materials#camera-and-view">the tutorial page section about cameras</a> for an explanation of camera controls.
* - **Text** - `WEBGL` requires opentype/truetype font files to be preloaded using <a href="#/p5/loadFont">loadFont()</a>. See <a href="https://github.com/processing/p5.js/wiki/Getting-started-with-WebGL-in-p5#text">the wiki section about text</a> for details, along with a workaround.
* - **Shaders** - Shaders are hardware accelerated programs that can be used for a variety of effects and graphics. See the <a href="https://p5js.org/tutorials/intro-to-shaders/">introduction to shaders</a> to get started with shaders in p5.js.
* - **Graphics Acceleration** - `WEBGL` mode uses the graphics card instead of the CPU, so it may help boost the performance of your sketch (example: drawing more shapes on the screen at once).
*
* To learn more about WEBGL mode, check out <a href="https://p5js.org/tutorials/#webgl">all the interactive WEBGL tutorials</a> in the "Tutorials" section of this website, or read the wiki article <a href="https://github.com/processing/p5.js/wiki/Getting-started-with-WebGL-in-p5">"Getting started with WebGL in p5"</a>.
*
* @typedef {'webgl'} WEBGL
* @property {WEBGL} WEBGL
* @final
*/
const WEBGL = 'webgl';
/**
* One of the two possible values of a WebGL canvas (either WEBGL or WEBGL2),
* which can be used to determine what capabilities the rendering environment
* has.
* @typedef {'webgl2'} WEBGL2
* @property {WEBGL2} WEBGL2
* @final
*/
const WEBGL2 = 'webgl2';
/**
* A constant used for creating a WebGPU rendering context
* @property {'webgpu'} WEBGPU
* @final
*/
const WEBGPU = 'webgpu';
// ENVIRONMENT
/**
* @typedef {'default'} ARROW
* @property {ARROW} ARROW
* @final
*/
const ARROW = 'default';
/**
* @property {String} SIMPLE
* @final
*/
const SIMPLE = 'simple';
/**
* @property {String} FULL
* @final
*/
const FULL = 'full';
/**
* @typedef {'crosshair'} CROSS
* @property {CROSS} CROSS
* @final
*/
const CROSS = 'crosshair';
/**
* @typedef {'pointer'} HAND
* @property {HAND} HAND
* @final
*/
const HAND = 'pointer';
/**
* @typedef {'move'} MOVE
* @property {MOVE} MOVE
* @final
*/
const MOVE = 'move';
/**
* @typedef {'text'} TEXT
* @property {TEXT} TEXT
* @final
*/
const TEXT = 'text';
/**
* @typedef {'wait'} WAIT
* @property {WAIT} WAIT
* @final
*/
const WAIT = 'wait';
// TRIGONOMETRY
/**
* A `Number` constant that's approximately 1.5708.
*
* `HALF_PI` is half the value of the mathematical constant π. It's useful for
* many tasks that involve rotation and oscillation. For example, calling
* `rotate(HALF_PI)` rotates the coordinate system `HALF_PI` radians, which is
* a quarter turn (90˚).
*
* Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
* radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
*
* @property {Number} HALF_PI
* @final
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Draw an arc from 0 to HALF_PI.
* arc(50, 50, 80, 80, 0, HALF_PI);
*
* describe('The bottom-right quarter of a circle drawn in white on a gray background.');
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Draw a line.
* line(0, 0, 40, 0);
*
* // Rotate a quarter turn.
* rotate(HALF_PI);
*
* // Draw the same line, rotated.
* line(0, 0, 40, 0);
*
* describe('Two black lines on a gray background. One line extends from the center to the right. The other line extends from the center to the bottom.');
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* describe(
* 'A red circle and a blue circle oscillate from left to right on a gray background. The red circle appears to chase the blue circle.'
* );
* }
*
* function draw() {
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Calculate the x-coordinates.
* let x1 = 40 * sin(frameCount * 0.05);
* let x2 = 40 * sin(frameCount * 0.05 + HALF_PI);
*
* // Style the oscillators.
* noStroke();
*
* // Draw the red oscillator.
* fill(255, 0, 0);
* circle(x1, 0, 20);
*
* // Draw the blue oscillator.
* fill(0, 0, 255);
* circle(x2, 0, 20);
* }
*/
const HALF_PI = _PI / 2;
/**
* A `Number` constant that's approximately 3.1416.
*
* `PI` is the mathematical constant π. It's useful for many tasks that
* involve rotation and oscillation. For example, calling `rotate(PI)` rotates
* the coordinate system `PI` radians, which is a half turn (180˚).
*
* Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
* radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
*
* @property {Number} PI
* @final
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Draw an arc from 0 to PI.
* arc(50, 50, 80, 80, 0, PI);
*
* describe('The bottom half of a circle drawn in white on a gray background.');
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Draw a line.
* line(0, 0, 40, 0);
*
* // Rotate a half turn.
* rotate(PI);
*
* // Draw the same line, rotated.
* line(0, 0, 40, 0);
*
* describe('A horizontal black line on a gray background.');
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* describe(
* 'A red circle and a blue circle oscillate from left to right on a gray background. The circles drift apart, then meet in the middle, over and over again.'
* );
* }
*
* function draw() {
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Calculate the x-coordinates.
* let x1 = 40 * sin(frameCount * 0.05);
* let x2 = 40 * sin(frameCount * 0.05 + PI);
*
* // Style the oscillators.
* noStroke();
*
* // Draw the red oscillator.
* fill(255, 0, 0);
* circle(x1, 0, 20);
*
* // Draw the blue oscillator.
* fill(0, 0, 255);
* circle(x2, 0, 20);
* }
*/
const PI = _PI;
/**
* A `Number` constant that's approximately 0.7854.
*
* `QUARTER_PI` is one-fourth the value of the mathematical constant π. It's
* useful for many tasks that involve rotation and oscillation. For example,
* calling `rotate(QUARTER_PI)` rotates the coordinate system `QUARTER_PI`
* radians, which is an eighth of a turn (45˚).
*
* Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
* radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
*
* @property {Number} QUARTER_PI
* @final
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Draw an arc from 0 to QUARTER_PI.
* arc(50, 50, 80, 80, 0, QUARTER_PI);
*
* describe('A one-eighth slice of a circle drawn in white on a gray background.');
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Draw a line.
* line(0, 0, 40, 0);
*
* // Rotate an eighth turn.
* rotate(QUARTER_PI);
*
* // Draw the same line, rotated.
* line(0, 0, 40, 0);
*
* describe('Two black lines that form a "V" opening towards the bottom-right corner of a gray square.');
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* describe(
* 'A red circle and a blue circle oscillate from left to right on a gray background. The red circle appears to chase the blue circle.'
* );
* }
*
* function draw() {
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Calculate the x-coordinates.
* let x1 = 40 * sin(frameCount * 0.05);
* let x2 = 40 * sin(frameCount * 0.05 + QUARTER_PI);
*
* // Style the oscillators.
* noStroke();
*
* // Draw the red oscillator.
* fill(255, 0, 0);
* circle(x1, 0, 20);
*
* // Draw the blue oscillator.
* fill(0, 0, 255);
* circle(x2, 0, 20);
* }
*/
const QUARTER_PI = _PI / 4;
/**
* A `Number` constant that's approximately 6.2382.
*
* `TAU` is twice the value of the mathematical constant π. It's useful for
* many tasks that involve rotation and oscillation. For example, calling
* `rotate(TAU)` rotates the coordinate system `TAU` radians, which is one
* full turn (360˚). `TAU` and `TWO_PI` are equal.
*
* Note: `TAU` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
* radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
*
* @property {Number} TAU
* @final
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Draw an arc from 0 to TAU.
* arc(50, 50, 80, 80, 0, TAU);
*
* describe('A white circle drawn on a gray background.');
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Draw a line.
* line(0, 0, 40, 0);
*
* // Rotate a full turn.
* rotate(TAU);
*
* // Style the second line.
* strokeWeight(5);
*
* // Draw the same line, shorter and rotated.
* line(0, 0, 20, 0);
*
* describe(
* 'Two horizontal black lines on a gray background. A thick line extends from the center toward the right. A thin line extends from the end of the thick line.'
* );
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* describe(
* 'A red circle with a blue center oscillates from left to right on a gray background.'
* );
* }
*
* function draw() {
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Calculate the x-coordinates.
* let x1 = 40 * sin(frameCount * 0.05);
* let x2 = 40 * sin(frameCount * 0.05 + TAU);
*
* // Style the oscillators.
* noStroke();
*
* // Draw the red oscillator.
* fill(255, 0, 0);
* circle(x1, 0, 20);
*
* // Draw the blue oscillator, smaller.
* fill(0, 0, 255);
* circle(x2, 0, 10);
* }
*/
const TAU = _PI * 2;
/**
* A `Number` constant that's approximately 6.2382.
*
* `TWO_PI` is twice the value of the mathematical constant π. It's useful for
* many tasks that involve rotation and oscillation. For example, calling
* `rotate(TWO_PI)` rotates the coordinate system `TWO_PI` radians, which is
* one full turn (360˚). `TWO_PI` and `TAU` are equal.
*
* Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
* radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
*
* @property {Number} TWO_PI
* @final
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Draw an arc from 0 to TWO_PI.
* arc(50, 50, 80, 80, 0, TWO_PI);
*
* describe('A white circle drawn on a gray background.');
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Draw a line.
* line(0, 0, 40, 0);
*
* // Rotate a full turn.
* rotate(TWO_PI);
*
* // Style the second line.
* strokeWeight(5);
*
* // Draw the same line, shorter and rotated.
* line(0, 0, 20, 0);
*
* describe(
* 'Two horizontal black lines on a gray background. A thick line extends from the center toward the right. A thin line extends from the end of the thick line.'
* );
* }
*
* @example
* function setup() {
* createCanvas(100, 100);
*
* describe(
* 'A red circle with a blue center oscillates from left to right on a gray background.'
* );
* }
*
* function draw() {
* background(200);
*
* // Translate the origin to the center.
* translate(50, 50);
*
* // Calculate the x-coordinates.
* let x1 = 40 * sin(frameCount * 0.05);
* let x2 = 40 * sin(frameCount * 0.05 + TWO_PI);
*
* // Style the oscillators.
* noStroke();
*
* // Draw the red oscillator.
* fill(255, 0, 0);
* circle(x1, 0, 20);
*
* // Draw the blue oscillator, smaller.
* fill(0, 0, 255);
* circle(x2, 0, 10);
* }
*/
const TWO_PI = _PI * 2;
/**
* @property {Number} DEG_TO_RAD
* @final
*/
const DEG_TO_RAD = _PI / 180.0;
/**
* @property {Number} RAD_TO_DEG
* @final
*/
const RAD_TO_DEG = 180.0 / _PI;
// SHAPE
/**
* @typedef {'corner'} CORNER
* @property {CORNER} CORNER
* @final
*/
const CORNER = 'corner';
/**
* @typedef {'corners'} CORNERS
* @property {CORNERS} CORNERS
* @final
*/
const CORNERS = 'corners';
/**
* @typedef {'radius'} RADIUS
* @property {RADIUS} RADIUS
* @final
*/
const RADIUS = 'radius';
/**
* @typedef {'right'} RIGHT
* @property {RIGHT} RIGHT
* @final
*/
const RIGHT = 'right';
/**
* @typedef {'left'} LEFT
* @property {LEFT} LEFT
* @final
*/
const LEFT = 'left';
/**
* @typedef {'center'} CENTER
* @property {CENTER} CENTER
* @final
*/
const CENTER = 'center';
/**
* @typedef {'top'} TOP
* @property {TOP} TOP
* @final
*/
const TOP = 'top';
/**
* @typedef {'bottom'} BOTTOM
* @property {BOTTOM} BOTTOM
* @final
*/
const BOTTOM = 'bottom';
/**
* @typedef {'alphabetic'} BASELINE
* @property {BASELINE} BASELINE
* @final
*/
const BASELINE = 'alphabetic';
/**
* @typedef {0x0000} POINTS
* @property {POINTS} POINTS
* @final
*/
const POINTS = 0x0000;
/**
* @typedef {0x0001} LINES
* @property {LINES} LINES
* @final
*/
const LINES = 0x0001;
/**
* @typedef {0x0003} LINE_STRIP
* @property {LINE_STRIP} LINE_STRIP
* @final
*/
const LINE_STRIP = 0x0003;
/**
* @typedef {0x0002} LINE_LOOP
* @property {LINE_LOOP} LINE_LOOP
* @final
*/
const LINE_LOOP = 0x0002;
/**
* @typedef {0x0004} TRIANGLES
* @property {TRIANGLES} TRIANGLES
* @final
*/
const TRIANGLES = 0x0004;
/**
* @typedef {0x0006} TRIANGLE_FAN
* @property {TRIANGLE_FAN} TRIANGLE_FAN
* @final
*/
const TRIANGLE_FAN = 0x0006;
/**
* @typedef {0x0005} TRIANGLE_STRIP
* @property {TRIANGLE_STRIP} TRIANGLE_STRIP
* @final
*/
const TRIANGLE_STRIP = 0x0005;
/**
* @typedef {'quads'} QUADS
* @property {QUADS} QUADS
* @final
*/
const QUADS = 'quads';
/**
* @typedef {'quad_strip'} QUAD_STRIP
* @property {QUAD_STRIP} QUAD_STRIP
* @final
*/
const QUAD_STRIP = 'quad_strip';
/**
* @typedef {'tess'} TESS
* @property {TESS} TESS
* @final
*/
const TESS = 'tess';
/**
* @typedef {0x0007} EMPTY_PATH
* @property {EMPTY_PATH} EMPTY_PATH
* @final
*/
const EMPTY_PATH = 0x0007;
/**
* @typedef {0x0008} PATH
* @property {PATH} PATH
* @final
*/
const PATH = 0x0008;
/**
* @typedef {'close'} CLOSE
* @property {CLOSE} CLOSE
* @final
*/
const CLOSE = 'close';
/**
* @typedef {'open'} OPEN
* @property {OPEN} OPEN
* @final
*/
const OPEN = 'open';
/**
* @typedef {'chord'} CHORD
* @property {CHORD} CHORD
* @final
*/
const CHORD = 'chord';
/**
* @typedef {'pie'} PIE
* @property {PIE} PIE
* @final
*/
const PIE = 'pie';
/**
* @typedef {'square'} PROJECT
* @property {PROJECT} PROJECT
* @final
*/
const PROJECT = 'square'; // PEND: careful this is counterintuitive
/**
* @typedef {'butt'} SQUARE
* @property {SQUARE} SQUARE
* @final
*/
const SQUARE = 'butt';
/**
* @typedef {'round'} ROUND
* @property {ROUND} ROUND
* @final
*/
const ROUND = 'round';
/**
* @typedef {'bevel'} BEVEL
* @property {BEVEL} BEVEL
* @final
*/
const BEVEL = 'bevel';
/**
* @typedef {'miter'} MITER
* @property {MITER} MITER
* @final
*/
const MITER = 'miter';
// DOM EXTENSION
/**
* AUTO allows us to automatically set the width or height of an element (but not both),
* based on the current height and width of the element. Only one parameter can
* be passed to the <a href="/reference/p5.Element/size">size</a> function as AUTO, at a time.
*
* @typedef {'auto'} AUTO
* @property {AUTO} AUTO
* @final
*/
const AUTO = 'auto';
// INPUT
/**
* @typedef {'Alt'} ALT
* @property {ALT} ALT
* @final
*/
const ALT = 'Alt';
/**
* @typedef {'Backspace'} BACKSPACE
* @property {BACKSPACE} BACKSPACE
* @final
*/
const BACKSPACE = 'Backspace';
/**
* @typedef {'Control' | 'Control'} CONTROL
* @property {CONTROL} CONTROL
* @final
*/
const CONTROL = 'Control';
/**
* @typedef {'Delete'} DELETE
* @property {DELETE} DELETE
* @final
*/
const DELETE = 'Delete';
/**
* @typedef {'ArrowDown'} DOWN_ARROW
* @property {DOWN_ARROW} DOWN_ARROW
* @final
*/
const DOWN_ARROW = 'ArrowDown';
/**
* @typedef {'Enter'} ENTER
* @property {ENTER} ENTER
* @final
*/
const ENTER = 'Enter';
/**
* @typedef {'Escape'} ESCAPE
* @property {ESCAPE} ESCAPE
* @final
*/
const ESCAPE = 'Escape';
/**
* @typedef {'ArrowLeft'} LEFT_ARROW
* @property {LEFT_ARROW} LEFT_ARROW
* @final
*/
const LEFT_ARROW = 'ArrowLeft';
/**
* @typedef {'Alt'} OPTION
* @property {OPTION} OPTION
* @final
*/
const OPTION = 'Alt';
/**
* @typedef {'Enter'} RETURN
* @property {RETURN} RETURN
* @final
*/
const RETURN = 'Enter';
/**
* @typedef {'ArrowRight'} RIGHT_ARROW
* @property {RIGHT_ARROW} RIGHT_ARROW
* @final
*/
const RIGHT_ARROW = 'ArrowRight';
/**
* @typedef {'Shift'} SHIFT
* @property {SHIFT} SHIFT
* @final
*/
const SHIFT = 'Shift';
/**
* @typedef {'Tab'} TAB
* @property {TAB} TAB
* @final
*/
const TAB = 'Tab';
/**
* @typedef {'ArrowUp'} UP_ARROW
* @property {UP_ARROW} UP_ARROW
* @final
*/
const UP_ARROW = 'ArrowUp';
// RENDERING
/**
* @typedef {'source-over'} BLEND
* @property {BLEND} BLEND
* @final
*/
const BLEND = 'source-over';
/**
* @typedef {'destination-out'} REMOVE
* @property {REMOVE} REMOVE
* @final
*/
const REMOVE = 'destination-out';
/**
* @typedef {'lighter'} ADD
* @property {ADD} ADD
* @final
*/
const ADD = 'lighter';
/**
* @typedef {'darken'} DARKEST
* @property {DARKEST} DARKEST
* @final
*/
const DARKEST = 'darken';
/**
* @typedef {'lighten'} LIGHTEST
* @property {LIGHTEST} LIGHTEST
* @final
*/
const LIGHTEST = 'lighten';
/**
* @typedef {'difference'} DIFFERENCE
* @property {DIFFERENCE} DIFFERENCE
* @final
*/
const DIFFERENCE = 'difference';
/**
* @typedef {'subtract'} SUBTRACT
* @property {SUBTRACT} SUBTRACT
* @final
*/
const SUBTRACT = 'subtract';
/**
* @typedef {'exclusion'} EXCLUSION
* @property {EXCLUSION} EXCLUSION
* @final
*/
const EXCLUSION = 'exclusion';
/**
* @typedef {'multiply'} MULTIPLY
* @property {MULTIPLY} MULTIPLY
* @final
*/
const MULTIPLY = 'multiply';
/**
* @typedef {'screen'} SCREEN
* @property {SCREEN} SCREEN
* @final
*/
const SCREEN = 'screen';
/**
* @typedef {'copy'} REPLACE
* @property {REPLACE} REPLACE
* @final
*/
const REPLACE = 'copy';
/**
* @typedef {'overlay'} OVERLAY
* @property {OVERLAY} OVERLAY
* @final
*/
const OVERLAY = 'overlay';
/**
* @typedef {'hard-light'} HARD_LIGHT
* @property {HARD_LIGHT} HARD_LIGHT
* @final
*/
const HARD_LIGHT = 'hard-light';
/**
* @typedef {'soft-light'} SOFT_LIGHT
* @property {SOFT_LIGHT} SOFT_LIGHT
* @final
*/
const SOFT_LIGHT = 'soft-light';
/**
* @typedef {'color-dodge'} DODGE
* @property {DODGE} DODGE
* @final
*/
const DODGE = 'color-dodge';
/**
* @typedef {'color-burn'} BURN
* @property {BURN} BURN
* @final
*/
const BURN = 'color-burn';
// FILTERS
/**
* @typedef {'threshold'} THRESHOLD
* @property {THRESHOLD} THRESHOLD
* @final
*/
const THRESHOLD = 'threshold';
/**
* @typedef {'gray'} GRAY
* @property {GRAY} GRAY
* @final
*/
const GRAY = 'gray';
/**
* @typedef {'opaque'} OPAQUE
* @property {OPAQUE} OPAQUE
* @final
*/
const OPAQUE = 'opaque';
/**
* @typedef {'invert'} INVERT
* @property {INVERT} INVERT
* @final
*/
const INVERT = 'invert';
/**
* @typedef {'posterize'} POSTERIZE
* @property {POSTERIZE} POSTERIZE
* @final
*/
const POSTERIZE = 'posterize';
/**
* @typedef {'dilate'} DILATE
* @property {DILATE} DILATE
* @final
*/
const DILATE = 'dilate';
/**
* @typedef {'erode'} ERODE
* @property {ERODE} ERODE
* @final
*/
const ERODE = 'erode';
/**
* @typedef {'blur'} BLUR
* @property {BLUR} BLUR
* @final
*/
const BLUR = 'blur';
// TYPOGRAPHY
/**
* @typedef {'normal'} NORMAL
* @property {NORMAL} NORMAL
* @final
*/
const NORMAL = 'normal';
/**
* @typedef {'italic'} ITALIC
* @property {ITALIC} ITALIC
* @final
*/
const ITALIC = 'italic';
/**
* @typedef {'bold'} BOLD
* @property {BOLD} BOLD
* @final
*/
const BOLD = 'bold';
/**
* @typedef {'bold italic'} BOLDITALIC
* @property {BOLDITALIC} BOLDITALIC
* @final
*/
const BOLDITALIC = 'bold italic';
/**
* @typedef {'CHAR'} CHAR
* @property {CHAR} CHAR
* @final
*/
const CHAR = 'CHAR';
/**
* @typedef {'WORD'} WORD
* @property {WORD} WORD
* @final
*/
const WORD = 'WORD';
// TYPOGRAPHY-INTERNAL
const _DEFAULT_TEXT_FILL = '#000000';
const _DEFAULT_LEADMULT = 1.25;
const _CTX_MIDDLE = 'middle';
// VERTICES
/**
* @typedef {'linear'} LINEAR
* @property {LINEAR} LINEAR
* @final
*/
const LINEAR = 'linear';
/**
* @typedef {'quadratic'} QUADRATIC
* @property {QUADRATIC} QUADRATIC
* @final
*/
const QUADRATIC = 'quadratic';
/**
* @typedef {'bezier'} BEZIER
* @property {BEZIER} BEZIER
* @final
*/
const BEZIER = 'bezier';
/**
* @typedef {'curve'} CURVE
* @property {CURVE} CURVE
* @final
*/
const CURVE = 'curve';
// WEBGL DRAWMODES
/**
* @typedef {'stroke'} STROKE
* @property {STROKE} STROKE
* @final
*/
const STROKE = 'stroke';
/**
* @typedef {'fill'} FILL
* @property {FILL} FILL
* @final
*/
const FILL = 'fill';
/**
* @typedef {'texture'} TEXTURE
* @property {TEXTURE} TEXTURE
* @final
*/
const TEXTURE = 'texture';
/**
* @typedef {'immediate'} IMMEDIATE
* @property {IMMEDIATE} IMMEDIATE
* @final
*/
const IMMEDIATE = 'immediate';
// WEBGL TEXTURE MODE
// NORMAL already exists for typography
/**
* @typedef {'image'} IMAGE
* @property {IMAGE} IMAGE
* @final
*/
const IMAGE = 'image';
// WEBGL TEXTURE WRAP AND FILTERING
// LINEAR already exists above
/**
* @typedef {'linear_mipmap'} LINEAR_MIPMAP
* @property {LINEAR_MIPMAP} LINEAR_MIPMAP
* @final
* @private
*/
const LINEAR_MIPMAP = 'linear_mipmap';
/**
* @typedef {'nearest'} NEAREST
* @property {NEAREST} NEAREST
* @final
*/
const NEAREST = 'nearest';
/**
* @typedef {'repeat'} REPEAT
* @property {REPEAT} REPEAT
* @final
*/
const REPEAT = 'repeat';
/**
* @typedef {'clamp'} CLAMP
* @property {CLAMP} CLAMP
* @final
*/
const CLAMP = 'clamp';
/**
* @typedef {'mirror'} MIRROR
* @property {MIRROR} MIRROR
* @final
*/
const MIRROR = 'mirror';
// WEBGL GEOMETRY SHADING
/**
* @typedef {'flat'} FLAT
* @property {FLAT} FLAT
* @final
*/
const FLAT = 'flat';
/**
* @typedef {'smooth'} SMOOTH
* @property {SMOOTH} SMOOTH
* @final
*/
const SMOOTH = 'smooth';
// DEVICE-ORIENTATION
/**
* @typedef {'landscape'} LANDSCAPE
* @property {LANDSCAPE} LANDSCAPE
* @final
*/
const LANDSCAPE = 'landscape';
/**
* @typedef {'portrait'} PORTRAIT
* @property {PORTRAIT} PORTRAIT
* @final
*/
const PORTRAIT = 'portrait';
// DEFAULTS
const _DEFAULT_STROKE = '#000000';
const _DEFAULT_FILL = '#FFFFFF';
/**
* @typedef {'grid'} GRID
* @property {GRID} GRID
* @final
*/
const GRID = 'grid';
/**
* @typedef {'axes'} AXES
* @property {AXES} AXES
* @final
*/
const AXES = 'axes';
/**
* @typedef {'label'} LABEL
* @property {LABEL} LABEL
* @final
*/
const LABEL = 'label';
/**
* @typedef {'fallback'} FALLBACK
* @property {FALLBACK} FALLBACK
* @final
*/
const FALLBACK = 'fallback';
/**
* @typedef {'contain'} CONTAIN
* @property {CONTAIN} CONTAIN
* @final
*/
const CONTAIN = 'contain';
/**
* @typedef {'cover'} COVER
* @property {COVER} COVER
* @final
*/
const COVER = 'cover';
/**
* @typedef {'unsigned-byte'} UNSIGNED_BYTE
* @property {UNSIGNED_BYTE} UNSIGNED_BYTE
* @final
*/
const UNSIGNED_BYTE = 'unsigned-byte';
/**
* @typedef {'unsigned-int'} UNSIGNED_INT
* @property {UNSIGNED_INT} UNSIGNED_INT
* @final
*/
const UNSIGNED_INT = 'unsigned-int';
/**
* @typedef {'float'} FLOAT
* @property {FLOAT} FLOAT
* @final
*/
const FLOAT = 'float';
/**
* @typedef {'half-float'} HALF_FLOAT
* @property {HALF_FLOAT} HALF_FLOAT
* @final
*/
const HALF_FLOAT = 'half-float';
/**
* The `splineProperty('ends')` mode where splines curve through
* their first and last points.
* @typedef {unique symbol} INCLUDE
* @property {INCLUDE} INCLUDE
* @final
*/
const INCLUDE = Symbol('include');
/**
* The `splineProperty('ends')` mode where the first and last points in a spline
* affect the direction of the curve, but are not rendered.
* @typedef {unique symbol} EXCLUDE
* @property {EXCLUDE} EXCLUDE
* @final
*/
const EXCLUDE = Symbol('exclude');
/**
* The `splineProperty('ends')` mode where the spline loops back to its first point.
* Only used internally.
* @typedef {unique symbol} JOIN
* @property {JOIN} JOIN
* @final
* @private
*/
const JOIN = Symbol('join');
var constants = /*#__PURE__*/Object.freeze({
__proto__: null,
ADD: ADD,
ALT: ALT,
ARROW: ARROW,
AUTO: AUTO,
AXES: AXES,
BACKSPACE: BACKSPACE,
BASELINE: BASELINE,
BEVEL: BEVEL,
BEZIER: BEZIER,
BLEND: BLEND,
BLUR: BLUR,
BOLD: BOLD,
BOLDITALIC: BOLDITALIC,
BOTTOM: BOTTOM,
BURN: BURN,
CENTER: CENTER,
CHAR: CHAR,
CHORD: CHORD,
CLAMP: CLAMP,
CLOSE: CLOSE,
CONTAIN: CONTAIN,
CONTROL: CONTROL,
CORNER: CORNER,
CORNERS: CORNERS,
COVER: COVER,
CROSS: CROSS,
CURVE: CURVE,
DARKEST: DARKEST,
DEG_TO_RAD: DEG_TO_RAD,
DELETE: DELETE,
DIFFERENCE: DIFFERENCE,
DILATE: DILATE,
DODGE: DODGE,
DOWN_ARROW: DOWN_ARROW,
EMPTY_PATH: EMPTY_PATH,
ENTER: ENTER,
ERODE: ERODE,
ESCAPE: ESCAPE,
EXCLUDE: EXCLUDE,
EXCLUSION: EXCLUSION,
FALLBACK: FALLBACK,
FILL: FILL,
FLAT: FLAT,
FLOAT: FLOAT,
FULL: FULL,
GRAY: GRAY,
GRID: GRID,
HALF_FLOAT: HALF_FLOAT,
HALF_PI: HALF_PI,
HAND: HAND,
HARD_LIGHT: HARD_LIGHT,
IMAGE: IMAGE,
IMMEDIATE: IMMEDIATE,
INCLUDE: INCLUDE,
INVERT: INVERT,
ITALIC: ITALIC,
JOIN: JOIN,
LABEL: LABEL,
LANDSCAPE: LANDSCAPE,
LEFT: LEFT,
LEFT_ARROW: LEFT_ARROW,
LIGHTEST: LIGHTEST,
LINEAR: LINEAR,
LINEAR_MIPMAP: LINEAR_MIPMAP,
LINES: LINES,
LINE_LOOP: LINE_LOOP,
LINE_STRIP: LINE_STRIP,
MIRROR: MIRROR,
MITER: MITER,
MOVE: MOVE,
MULTIPLY: MULTIPLY,
NEAREST: NEAREST,
NORMAL: NORMAL,
OPAQUE: OPAQUE,
OPEN: OPEN,
OPTION: OPTION,
OVERLAY: OVERLAY,
P2D: P2D,
P2DP3: P2DP3,
PATH: PATH,
PI: PI,
PIE: PIE,
POINTS: POINTS,
PORTRAIT: PORTRAIT,
POSTERIZE: POSTERIZE,
PROJECT: PROJECT,
QUADRATIC: QUADRATIC,
QUADS: QUADS,
QUAD_STRIP: QUAD_STRIP,
QUARTER_PI: QUARTER_PI,
RADIUS: RADIUS,
RAD_TO_DEG: RAD_TO_DEG,
REMOVE: REMOVE,
REPEAT: REPEAT,
REPLACE: REPLACE,
RETURN: RETURN,
RIGHT: RIGHT,
RIGHT_ARROW: RIGHT_ARROW,
ROUND: ROUND,
SCREEN: SCREEN,
SHIFT: SHIFT,
SIMPLE: SIMPLE,
SMOOTH: SMOOTH,
SOFT_LIGHT: SOFT_LIGHT,
SQUARE: SQUARE,
STROKE: STROKE,
SUBTRACT: SUBTRACT,
TAB: TAB,
TAU: TAU,
TESS: TESS,
TEXT: TEXT,
TEXTURE: TEXTURE,
THRESHOLD: THRESHOLD,
TOP: TOP,
TRIANGLES: TRIANGLES,
TRIANGLE_FAN: TRIANGLE_FAN,
TRIANGLE_STRIP: TRIANGLE_STRIP,
TWO_PI: TWO_PI,
UNSIGNED_BYTE: UNSIGNED_BYTE,
UNSIGNED_INT: UNSIGNED_INT,
UP_ARROW: UP_ARROW,
VERSION: VERSION,
WAIT: WAIT,
WEBGL: WEBGL,
WEBGL2: WEBGL2,
WEBGPU: WEBGPU,
WORD: WORD,
_CTX_MIDDLE: _CTX_MIDDLE,
_DEFAULT_FILL: _DEFAULT_FILL,
_DEFAULT_LEADMULT: _DEFAULT_LEADMULT,
_DEFAULT_STROKE: _DEFAULT_STROKE,
_DEFAULT_TEXT_FILL: _DEFAULT_TEXT_FILL
});
function getStrokeDefs(shaderConstant) {
const STROKE_CAP_ENUM = {};
const STROKE_JOIN_ENUM = {};
let lineDefs = "";
const defineStrokeCapEnum = function (key, val) {
lineDefs += shaderConstant(`STROKE_CAP_${key}`, `${val}`, 'u32');
STROKE_CAP_ENUM[constants[key]] = val;
};
const defineStrokeJoinEnum = function (key, val) {
lineDefs += shaderConstant(`STROKE_JOIN_${key}`, `${val}`, 'u32');
STROKE_JOIN_ENUM[constants[key]] = val;
};
// Define constants in line shaders for each type of cap/join, and also record
// the values in JS objects
defineStrokeCapEnum("ROUND", 0);
defineStrokeCapEnum("PROJECT", 1);
defineStrokeCapEnum("SQUARE", 2);
defineStrokeJoinEnum("ROUND", 0);
defineStrokeJoinEnum("MITER", 1);
defineStrokeJoinEnum("BEVEL", 2);
return { STROKE_CAP_ENUM, STROKE_JOIN_ENUM, lineDefs };
}
/////////////////////
// Enums for nodes //
/////////////////////
const NodeType = {
OPERATION: 'operation',
LITERAL: 'literal',
VARIABLE: 'variable',
CONSTANT: 'constant',
STRUCT: 'struct',
PHI: 'phi',
STATEMENT: 'statement',
ASSIGNMENT: 'assignment',
};
const INSTANCE_ID_VARYING_NAME = '_p5_instanceID';
const HOOK_PARAM_PREFIX = '_p5_param_';
const NodeTypeToName = Object.fromEntries(
Object.entries(NodeType).map(([key, val]) => [val, key])
);
const NodeTypeRequiredFields = {
[NodeType.OPERATION]: ["opCode", "dependsOn", "dimension", "baseType"],
[NodeType.LITERAL]: ["value", "dimension", "baseType"],
[NodeType.VARIABLE]: ["identifier", "dimension", "baseType"],
[NodeType.CONSTANT]: ["value", "dimension", "baseType"],
[NodeType.STRUCT]: [""],
[NodeType.PHI]: ["dependsOn", "phiBlocks", "dimension", "baseType"],
[NodeType.STATEMENT]: ["statementType"],
[NodeType.ASSIGNMENT]: ["dependsOn"]
};
const StatementType = {
DISCARD: 'discard',
BREAK: 'break',
EARLY_RETURN: 'early_return',
EXPRESSION: 'expression', // Used when we want to output a single expression as a statement, e.g. a for loop condition
EMPTY: 'empty', // Used for empty statements like ; in for loops
};
const BaseType = {
FLOAT: "float",
INT: "int",
BOOL: "bool",
MAT: "mat",
DEFER: "defer",
ASSIGN_ON_USE: "assign_on_use",
SAMPLER2D: "sampler2D",
SAMPLER: "sampler",
};
const BasePriority = {
[BaseType.FLOAT]: 3,
[BaseType.INT]: 2,
[BaseType.BOOL]: 1,
[BaseType.MAT]: 0,
[BaseType.DEFER]: -1,
[BaseType.ASSIGN_ON_USE]: -2,
[BaseType.SAMPLER2D]: -10,
[BaseType.SAMPLER]: -11,
};
const DataType = {
float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, },
float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3, },
float3: { fnName: "vec3", baseType: BaseType.FLOAT, dimension:3, priority: 3, },
float4: { fnName: "vec4", baseType: BaseType.FLOAT, dimension:4, priority: 3, },
int1: { fnName: "int", baseType: BaseType.INT, dimension:1, priority: 2, },
int2: { fnName: "ivec2", baseType: BaseType.INT, dimension:2, priority: 2, },
int3: { fnName: "ivec3", baseType: BaseType.INT, dimension:3, priority: 2, },
int4: { fnName: "ivec4", baseType: BaseType.INT, dimension:4, priority: 2, },
bool1: { fnName: "bool", baseType: BaseType.BOOL, dimension:1, priority: 1, },
bool2: { fnName: "bvec2", baseType: BaseType.BOOL, dimension:2, priority: 1, },
bool3: { fnName: "bvec3", baseType: BaseType.BOOL, dimension:3, priority: 1, },
bool4: { fnName: "bvec4", baseType: BaseType.BOOL, dimension:4, priority: 1, },
mat2: { fnName: "mat2x2", baseType: BaseType.MAT, dimension:2, priority: 0, },
mat3: { fnName: "mat3x3", baseType: BaseType.MAT, dimension:3, priority: 0, },
mat4: { fnName: "mat4x4", baseType: BaseType.MAT, dimension:4, priority: 0, },
defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 },
assign_on_use: { fnName: null, baseType: BaseType.ASSIGN_ON_USE, dimension: null, priority: -2 },
sampler2D: { fnName: "sampler2D", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 },
sampler: { fnName: "sampler", baseType: BaseType.SAMPLER, dimension: 1, priority: -11 },
};
function isStructType(typeInfo) {
return !!(typeInfo && typeInfo.properties);
}
const GenType = {
FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 },
INT: { baseType: BaseType.INT, dimension: null, priority: 2 },
BOOL: { baseType: BaseType.BOOL, dimension: null, priority: 1 },
};
function typeEquals(nodeA, nodeB) {
return (nodeA.dimension === nodeB.dimension) && (nodeA.baseType === nodeB.baseType);
}
Object.fromEntries(
Object.values(DataType)
.filter(info => info.fnName !== null)
.map(info => [info.fnName, info])
);
const OpCode = {
Binary: {
ADD: 0,
SUBTRACT: 1,
MULTIPLY: 2,
DIVIDE: 3,
MODULO: 4,
EQUAL: 5,
NOT_EQUAL: 6,
GREATER_THAN: 7,
GREATER_EQUAL: 8,
LESS_THAN: 9,
LESS_EQUAL: 10,
LOGICAL_AND: 11,
LOGICAL_OR: 12,
MEMBER_ACCESS: 13,
ARRAY_ACCESS: 14,
},
Unary: {
LOGICAL_NOT: 100,
NEGATE: 101,
PLUS: 102,
SWIZZLE: 103,
},
Nary: {
FUNCTION_CALL: 200,
CONSTRUCTOR: 201,
TERNARY: 202}};
const OperatorTable = [
{ arity: "unary", boolean: true, name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT },
{ arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE },
{ arity: "unary", name: "plus", symbol: "+", opCode: OpCode.Unary.PLUS },
{ arity: "binary", name: "add", symbol: "+", opCode: OpCode.Binary.ADD },
{ arity: "binary", name: "sub", symbol: "-", opCode: OpCode.Binary.SUBTRACT },
{ arity: "binary", name: "mult", symbol: "*", opCode: OpCode.Binary.MULTIPLY },
{ arity: "binary", name: "div", symbol: "/", opCode: OpCode.Binary.DIVIDE },
{ arity: "binary", name: "mod", symbol: "%", opCode: OpCode.Binary.MODULO },
{ arity: "binary", boolean: true, name: "equalTo", symbol: "==", opCode: OpCode.Binary.EQUAL },
{ arity: "binary", boolean: true, name: "notEqual", symbol: "!=", opCode: OpCode.Binary.NOT_EQUAL },
{ arity: "binary", boolean: true, name: "greaterThan", symbol: ">", opCode: OpCode.Binary.GREATER_THAN },
{ arity: "binary", boolean: true, name: "greaterEqual", symbol: ">=", opCode: OpCode.Binary.GREATER_EQUAL },
{ arity: "binary", boolean: true, name: "lessThan", symbol: "<", opCode: OpCode.Binary.LESS_THAN },
{ arity: "binary", boolean: true, name: "lessEqual", symbol: "<=", opCode: OpCode.Binary.LESS_EQUAL },
{ arity: "binary", boolean: true, name: "and", symbol: "&&", opCode: OpCode.Binary.LOGICAL_AND },
{ arity: "binary", boolean: true, name: "or", symbol: "||", opCode: OpCode.Binary.LOGICAL_OR },
];
// export const SymbolToOpCode = {};
const OpCodeToSymbol = {};
for (const { symbol, opCode, name, arity, boolean } of OperatorTable) {
// SymbolToOpCode[symbol] = opCode;
OpCodeToSymbol[opCode] = symbol;
}
const BlockType = {
GLOBAL: 'global',
FUNCTION: 'function',
BRANCH: 'branch',
IF_COND: 'if_cond',
IF_BODY: 'if_body',
ELSE_COND: 'else_cond',
SCOPE_START: 'scope_start',
SCOPE_END: 'scope_end',
FOR: 'for',
MERGE: 'merge',
DEFAULT: 'default',
};
Object.fromEntries(
Object.entries(BlockType).map(([key, val]) => [val, key])
);
const uniforms$5 = `
// Group 0: Material Properties
struct MaterialUniforms {
uUseVertexColor: u32,
}
// Group 1: Model Transform
struct ModelUniforms {
// @p5 ifdef Vertex getWorldInputs
uModelMatrix: mat4x4<f32>,
uModelNormalMatrix: mat3x3<f32>,
// @p5 endif
// @p5 ifndef Vertex getWorldInputs
uModelViewMatrix: mat4x4<f32>,
uNormalMatrix: mat3x3<f32>,
// @p5 endif
uMaterialColor: vec4<f32>,
}
// Group 2: Camera and Projection
struct CameraUniforms {
uProjectionMatrix: mat4x4<f32>,
// @p5 ifdef Vertex getWorldInputs
uViewMatrix: mat4x4<f32>,
// @p5 endif
uCameraNormalMatrix: mat3x3<f32>,
}
`;
const colorVertexShader = `
struct VertexInput {
@location(0) aPosition: vec3<f32>,
@location(1) aNormal: vec3<f32>,
@location(2) aTexCoord: vec2<f32>,
@location(3) aVertexColor: vec4<f32>,
};
struct VertexOutput {
@builtin(position) Position: vec4<f32>,
@location(0) vVertexNormal: vec3<f32>,
@location(1) vVertTexCoord: vec2<f32>,
@location(2) vColor: vec4<f32>,
};
${uniforms$5}
@group(0) @binding(0) var<uniform> material: MaterialUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
struct Vertex {
position: vec3<f32>,
normal: vec3<f32>,
texCoord: vec2<f32>,
color: vec4<f32>,
}
@vertex
fn main(input: VertexInput) -> VertexOutput {
HOOK_beforeVertex();
var output: VertexOutput;
let useVertexColor = (material.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0);
var inputs = Vertex(
input.aPosition,
input.aNormal,
input.aTexCoord,
select(model.uMaterialColor, input.aVertexColor, useVertexColor)
);
// @p5 ifdef Vertex getObjectInputs
inputs = HOOK_getObjectInputs(inputs);
// @p5 endif
// @p5 ifdef Vertex getWorldInputs
inputs.position = (model.uModelMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
inputs.normal = model.uModelNormalMatrix * inputs.normal;
inputs = HOOK_getWorldInputs(inputs);
// @p5 endif
// @p5 ifdef Vertex getWorldInputs
// Already multiplied by the model matrix, just apply view
inputs.position = (camera.uViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
inputs.normal = camera.uCameraNormalMatrix * inputs.normal;
// @p5 endif
// @p5 ifndef Vertex getWorldInputs
// Apply both at once
inputs.position = (model.uModelViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
inputs.normal = model.uNormalMatrix * inputs.normal;
// @p5 endif
// @p5 ifdef Vertex getCameraInputs
inputs = HOOK_getCameraInputs(inputs);
// @p5 endif
output.vVertTexCoord = inputs.texCoord;
output.vVertexNormal = normalize(inputs.normal);
output.vColor = inputs.color;
output.Position = camera.uProjectionMatrix * vec4<f32>(inputs.position, 1.0);
HOOK_afterVertex();
return output;
}
`;
const colorFragmentShader = `
struct FragmentInput {
@location(0) vVertexNormal: vec3<f32>,
@location(1) vVertTexCoord: vec2<f32>,
@location(2) vColor: vec4<f32>,
};
${uniforms$5}
@group(0) @binding(0) var<uniform> material: MaterialUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
@fragment
fn main(input: FragmentInput) -> @location(0) vec4<f32> {
HOOK_beforeFragment();
var outColor = HOOK_getFinalColor(input.vColor, input.vVertTexCoord);
outColor = vec4<f32>(outColor.rgb * outColor.a, outColor.a);
HOOK_afterFragment();
return outColor;
}
`;
const uniforms$4 = `
// Group 0: Stroke Properties
struct StrokeUniforms {
uStrokeWeight: f32,
uUseLineColor: f32,
uSimpleLines: f32,
uStrokeCap: u32,
uStrokeJoin: u32,
}
// Group 1: Model Transform
struct ModelUniforms {
// @p5 ifdef StrokeVertex getWorldInputs
uModelMatrix: mat4x4<f32>,
uViewMatrix: mat4x4<f32>,
// @p5 endif
// @p5 ifndef StrokeVertex getWorldInputs
uModelViewMatrix: mat4x4<f32>,
// @p5 endif
uMaterialColor: vec4<f32>,
}
// Group 2: Camera and Projection
struct CameraUniforms {
uProjectionMatrix: mat4x4<f32>,
uViewport: vec4<f32>,
uPerspective: u32,
}
`;
const lineVertexShader = `
struct StrokeVertexInput {
@location(0) aPosition: vec3<f32>,
@location(1) aSide: f32,
@location(2) aTangentIn: vec3<f32>,
@location(3) aTangentOut: vec3<f32>,
@location(4) aVertexColor: vec4<f32>,
};
struct StrokeVertexOutput {
@builtin(position) Position: vec4<f32>,
@location(0) vColor: vec4<f32>,
@location(1) vTangent: vec2<f32>,
@location(2) vCenter: vec2<f32>,
@location(3) vPosition: vec2<f32>,
@location(4) vMaxDist: f32,
@location(5) vCap: f32,
@location(6) vJoin: f32,
@location(7) vStrokeWeight: f32,
};
${uniforms$4}
@group(0) @binding(0) var<uniform> stroke: StrokeUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
struct StrokeVertex {
position: vec3<f32>,
tangentIn: vec3<f32>,
tangentOut: vec3<f32>,
color: vec4<f32>,
weight: f32,
}
fn lineIntersection(aPoint: vec2f, aDir: vec2f, bPoint: vec2f, bDir: vec2f) -> vec2f {
// Rotate and translate so a starts at the origin and goes out to the right
var bMutPoint = bPoint;
bMutPoint -= aPoint;
var rotatedBFrom = vec2<f32>(
bMutPoint.x*aDir.x + bMutPoint.y*aDir.y,
bMutPoint.y*aDir.x - bMutPoint.x*aDir.y
);
var bTo = bMutPoint + bDir;
var rotatedBTo = vec2<f32>(
bTo.x*aDir.x + bTo.y*aDir.y,
bTo.y*aDir.x - bTo.x*aDir.y
);
var intersectionDistance =
rotatedBTo.x + (rotatedBFrom.x - rotatedBTo.x) * rotatedBTo.y /
(rotatedBTo.y - rotatedBFrom.y);
return aPoint + aDir * intersectionDistance;
}
@vertex
fn main(input: StrokeVertexInput) -> StrokeVertexOutput {
HOOK_beforeVertex();
var output: StrokeVertexOutput;
let simpleLines = (stroke.uSimpleLines != 0.);
if (!simpleLines) {
if (all(input.aTangentIn == vec3<f32>()) != all(input.aTangentOut == vec3<f32>())) {
output.vCap = 1.;
} else {
output.vCap = 0.;
}
let conditionA = any(input.aTangentIn != vec3<f32>());
let conditionB = any(input.aTangentOut != vec3<f32>());
let conditionC = any(input.aTangentIn != input.aTangentOut);
if (conditionA && conditionB && conditionC) {
output.vJoin = 1.;
} else {
output.vJoin = 0.;
}
}
var lineColor: vec4<f32>;
if (stroke.uUseLineColor != 0.) {
lineColor = input.aVertexColor;
} else {
lineColor = model.uMaterialColor;
}
var inputs = StrokeVertex(
input.aPosition.xyz,
input.aTangentIn,
input.aTangentOut,
lineColor,
stroke.uStrokeWeight
);
// @p5 ifdef StrokeVertex getObjectInputs
inputs = HOOK_getObjectInputs(inputs);
// @p5 endif
// @p5 ifdef StrokeVertex getWorldInputs
inputs.position = (model.uModelMatrix * vec4<f32>(inputs.position, 1.)).xyz;
inputs.tangentIn = (model.uModelMatrix * vec4<f32>(input.aTangentIn, 1.)).xyz;
inputs.tangentOut = (model.uModelMatrix * vec4<f32>(input.aTangentOut, 1.)).xyz;
inputs = HOOK_getWorldInputs(inputs);
// @p5 endif
// @p5 ifdef StrokeVertex getWorldInputs
// Already multiplied by the model matrix, just apply view
inputs.position = (model.uViewMatrix * vec4<f32>(inputs.position, 1.)).xyz;
inputs.tangentIn = (model.uViewMatrix * vec4<f32>(input.aTangentIn, 0.)).xyz;
inputs.tangentOut = (model.uViewMatrix * vec4<f32>(input.aTangentOut, 0.)).xyz;
// @p5 endif
// @p5 ifndef StrokeVertex getWorldInputs
// Apply both at once
inputs.position = (model.uModelViewMatrix * vec4<f32>(inputs.position, 1.)).xyz;
inputs.tangentIn = (model.uModelViewMatrix * vec4<f32>(input.aTangentIn, 0.)).xyz;
inputs.tangentOut = (model.uModelViewMatrix * vec4<f32>(input.aTangentOut, 0.)).xyz;
// @p5 endif
// @p5 ifdef StrokeVertex getCameraInputs
inputs = HOOK_getCameraInputs(inputs);
// @p5 endif
var posp = vec4<f32>(inputs.position, 1.);
var posqIn = vec4<f32>(inputs.position + inputs.tangentIn, 1.);
var posqOut = vec4<f32>(inputs.position + inputs.tangentOut, 1.);
output.vStrokeWeight = inputs.weight;
var facingCamera = pow(
// The word space tangent's z value is 0 if it's facing the camera
abs(normalize(posqIn-posp).z),
// Using pow() here to ramp 'facingCamera' up from 0 to 1 really quickly
// so most lines get scaled and don't get clipped
0.25
);
// Moving vertices slightly toward the camera
// to avoid depth-fighting with the fill triangles.
// A mix of scaling and offsetting is used based on distance
// Discussion here:
// https://github.com/processing/p5.js/issues/7200
// using a scale <1 moves the lines towards nearby camera
// in order to prevent popping effects due to half of
// the line disappearing behind the geometry faces.
var zDistance = -posp.z;
var distanceFactor = smoothstep(0., 800., zDistance);
// Discussed here:
// http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=252848
var scale = mix(1., 0.995, facingCamera);
var dynamicScale = mix(scale, 1.0, distanceFactor); // Closer = more scale, farther = less
posp = vec4<f32>(posp.xyz * dynamicScale, posp.w);
posqIn = vec4<f32>(posqIn.xyz * dynamicScale, posqIn.w);
posqOut= vec4<f32>(posqOut.xyz * dynamicScale, posqOut.w);
// Moving vertices slightly toward camera when far away
// https://github.com/processing/p5.js/issues/6956
var zOffset = mix(0., -1., facingCamera);
var dynamicZAdjustment = mix(0., zOffset, distanceFactor); // Closer = less zAdjustment, farther = more
posp.z -= dynamicZAdjustment;
posqIn.z -= dynamicZAdjustment;
posqOut.z -= dynamicZAdjustment;
var p = camera.uProjectionMatrix * posp;
var qIn = camera.uProjectionMatrix * posqIn;
var qOut = camera.uProjectionMatrix * posqOut;
var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * camera.uViewport.zw);
var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * camera.uViewport.zw);
var curPerspScale = vec2<f32>();
if (camera.uPerspective == 1) {
// Perspective ---
// convert from world to clip by multiplying with projection scaling factor
// to get the right thickness (see https://github.com/processing/processing/issues/5182)
// The y value of the projection matrix may be flipped if rendering to a Framebuffer.
// Multiplying again by its sign here negates the flip to get just the scale.
curPerspScale = (camera.uProjectionMatrix * vec4(1., sign(camera.uProjectionMatrix[1][1]), 0., 0.)).xy;
} else {
// No Perspective ---
// multiply by W (to cancel out division by W later in the pipeline) and
// convert from screen to clip (derived from clip to screen above)
curPerspScale = p.w / (0.5 * camera.uViewport.zw);
}
var offset = vec2<f32>();
if (output.vJoin == 1. && !simpleLines) {
output.vTangent = normalize(tangentIn + tangentOut);
var normalIn = vec2<f32>(-tangentIn.y, tangentIn.x);
var normalOut = vec2<f32>(-tangentOut.y, tangentOut.x);
var side = sign(input.aSide);
var sideEnum = abs(input.aSide);
// We generate vertices for joins on either side of the centerline, but
// the "elbow" side is the only one needing a join. By not setting the
// offset for the other side, all its vertices will end up in the same
// spot and not render, effectively discarding it.
if (sign(dot(tangentOut, vec2<f32>(-tangentIn.y, tangentIn.x))) != side) {
// Side enums:
// 1: the side going into the join
// 2: the middle of the join
// 3: the side going out of the join
if (sideEnum == 2.) {
// Calculate the position + tangent on either side of the join, and
// find where the lines intersect to find the elbow of the join
var c = (posp.xy / posp.w + vec2<f32>(1.)) * 0.5 * camera.uViewport.zw;
var intersection = lineIntersection(
c + (side * normalIn * inputs.weight / 2.),
tangentIn,
c + (side * normalOut * inputs.weight / 2.),
tangentOut
);
offset = intersection - c;
// When lines are thick and the angle of the join approaches 180, the
// elbow might be really far from the center. We'll apply a limit to
// the magnitude to avoid lines going across the whole screen when this
// happens.
var mag = length(offset);
var maxMag = 3. * inputs.weight;
if (mag > maxMag) {
offset *= maxMag / mag;
}
} else if (sideEnum == 1.) {
offset = side * normalIn * inputs.weight / 2.;
} else if (sideEnum == 3.) {
offset = side * normalOut * inputs.weight / 2.;
}
}
if (stroke.uStrokeJoin == 2) {
var avgNormal = vec2<f32>(-output.vTangent.y, output.vTangent.x);
output.vMaxDist = abs(dot(avgNormal, normalIn * inputs.weight / 2.));
} else {
output.vMaxDist = inputs.weight / 2.;
}
} else {
var tangent: vec2<f32>;
if (all(input.aTangentIn == vec3<f32>())) {
tangent = tangentOut;
} else {
tangent = tangentIn;
}
output.vTangent = tangent;
var normal = vec2<f32>(-tangent.y, tangent.x);
var normalOffset = sign(input.aSide);
// Caps will have side values of -2 or 2 on the edge of the cap that
// extends out from the line
var tangentOffset = abs(input.aSide) - 1.;
offset = (normal * normalOffset + tangent * tangentOffset) *
inputs.weight * 0.5;
output.vMaxDist = inputs.weight / 2.;
}
output.vCenter = p.xy;
output.vPosition = output.vCenter + offset;
output.vColor = inputs.color;
output.Position = vec4<f32>(
p.xy + offset.xy * curPerspScale,
p.zw
);
HOOK_afterVertex();
return output;
}`;
const lineFragmentShader = `
struct StrokeFragmentInput {
@location(0) vColor: vec4<f32>,
@location(1) vTangent: vec2<f32>,
@location(2) vCenter: vec2<f32>,
@location(3) vPosition: vec2<f32>,
@location(4) vMaxDist: f32,
@location(5) vCap: f32,
@location(6) vJoin: f32,
@location(7) vStrokeWeight: f32,
}
${uniforms$4}
@group(0) @binding(0) var<uniform> stroke: StrokeUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
fn distSquared(a: vec2<f32>, b: vec2<f32>) -> f32 {
return dot(b - a, b - a);
}
struct Inputs {
color: vec4<f32>,
tangent: vec2<f32>,
center: vec2<f32>,
position: vec2<f32>,
strokeWeight: f32,
}
@fragment
fn main(input: StrokeFragmentInput) -> @location(0) vec4<f32> {
HOOK_beforeFragment();
var inputs: Inputs;
inputs.color = input.vColor;
inputs.tangent = input.vTangent;
inputs.center = input.vCenter;
inputs.position = input.vPosition;
inputs.strokeWeight = input.vStrokeWeight;
inputs = HOOK_getPixelInputs(inputs);
if (input.vCap > 0.) {
if (
stroke.uStrokeCap == STROKE_CAP_ROUND &&
HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25)
) {
discard;
} else if (
stroke.uStrokeCap == STROKE_CAP_SQUARE &&
HOOK_shouldDiscard(dot(inputs.position - inputs.center, inputs.tangent) > 0.)
) {
discard;
} else if (HOOK_shouldDiscard(false)) {
discard;
}
} else if (input.vJoin > 0.) {
if (
stroke.uStrokeJoin == STROKE_JOIN_ROUND &&
HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25)
) {
discard;
} else if (stroke.uStrokeJoin == STROKE_JOIN_BEVEL) {
let normal = vec2<f32>(-inputs.tangent.y, -inputs.tangent.x);
if (HOOK_shouldDiscard(abs(dot(inputs.position - inputs.center, normal)) > input.vMaxDist)) {
discard;
}
} else if (HOOK_shouldDiscard(false)) {
discard;
}
}
var col = HOOK_getFinalColor(inputs.color, vec2<f32>(0.0, 0.0));
col = vec4<f32>(col.rgb, 1.0) * col.a;
HOOK_afterFragment();
return vec4<f32>(col);
}
`;
const uniforms$3 = `
// Group 0: Material Properties
struct MaterialUniforms {
uUseVertexColor: u32,
uHasSetAmbient: u32,
uAmbientColor: vec3<f32>,
uSpecularMatColor: vec4<f32>,
uAmbientMatColor: vec4<f32>,
uEmissiveMatColor: vec4<f32>,
uTint: vec4<f32>,
isTexture: u32,
uSpecular: u32,
uShininess: f32,
uMetallic: f32,
}
// Group 0: Lighting
struct LightingUniforms {
uDirectionalLightCount: i32,
uLightingDirection: array<vec3<f32>, 5>,
uDirectionalDiffuseColors: array<vec3<f32>, 5>,
uDirectionalSpecularColors: array<vec3<f32>, 5>,
uPointLightCount: i32,
uPointLightLocation: array<vec3<f32>, 5>,
uPointLightDiffuseColors: array<vec3<f32>, 5>,
uPointLightSpecularColors: array<vec3<f32>, 5>,
uSpotLightCount: i32,
uSpotLightAngle: vec4<f32>,
uSpotLightConc: vec4<f32>,
uSpotLightDiffuseColors: array<vec3<f32>, 4>,
uSpotLightSpecularColors: array<vec3<f32>, 4>,
uSpotLightLocation: array<vec3<f32>, 4>,
uSpotLightDirection: array<vec3<f32>, 4>,
uConstantAttenuation: f32,
uLinearAttenuation: f32,
uQuadraticAttenuation: f32,
uUseImageLight: u32,
uUseLighting: u32,
}
// Group 1: Model Transform
struct ModelUniforms {
// @p5 ifdef Vertex getWorldInputs
uModelMatrix: mat4x4<f32>,
uModelNormalMatrix: mat3x3<f32>,
// @p5 endif
// @p5 ifndef Vertex getWorldInputs
uModelViewMatrix: mat4x4<f32>,
uNormalMatrix: mat3x3<f32>,
// @p5 endif
uMaterialColor: vec4<f32>,
}
// Group 2: Camera and Projection
struct CameraUniforms {
uViewMatrix: mat4x4<f32>,
uProjectionMatrix: mat4x4<f32>,
uCameraNormalMatrix: mat3x3<f32>,
}
`;
const materialVertexShader = `
struct VertexInput {
@location(0) aPosition: vec3<f32>,
@location(1) aNormal: vec3<f32>,
@location(2) aTexCoord: vec2<f32>,
@location(3) aVertexColor: vec4<f32>,
};
struct VertexOutput {
@builtin(position) Position: vec4<f32>,
@location(0) vNormal: vec3<f32>,
@location(1) vTexCoord: vec2<f32>,
@location(2) vViewPosition: vec3<f32>,
@location(4) vColor: vec4<f32>,
};
${uniforms$3}
@group(0) @binding(0) var<uniform> material: MaterialUniforms;
@group(0) @binding(1) var<uniform> lighting: LightingUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
struct Vertex {
position: vec3<f32>,
normal: vec3<f32>,
texCoord: vec2<f32>,
color: vec4<f32>,
}
@vertex
fn main(input: VertexInput) -> VertexOutput {
HOOK_beforeVertex();
var output: VertexOutput;
let useVertexColor = (material.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0);
var inputs = Vertex(
input.aPosition,
input.aNormal,
input.aTexCoord,
select(model.uMaterialColor, input.aVertexColor, useVertexColor)
);
// @p5 ifdef Vertex getObjectInputs
inputs = HOOK_getObjectInputs(inputs);
// @p5 endif
// @p5 ifdef Vertex getWorldInputs
inputs.position = (model.uModelMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
inputs.normal = model.uModelNormalMatrix * inputs.normal;
inputs = HOOK_getWorldInputs(inputs);
// @p5 endif
// @p5 ifdef Vertex getWorldInputs
// Already multiplied by the model matrix, just apply view
inputs.position = (camera.uViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
inputs.normal = camera.uCameraNormalMatrix * inputs.normal;
// @p5 endif
// @p5 ifndef Vertex getWorldInputs
// Apply both at once
inputs.position = (model.uModelViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
inputs.normal = model.uNormalMatrix * inputs.normal;
// @p5 endif
// @p5 ifdef Vertex getCameraInputs
inputs = HOOK_getCameraInputs(inputs);
// @p5 endif
output.vViewPosition = inputs.position;
output.vTexCoord = inputs.texCoord;
output.vNormal = normalize(inputs.normal);
output.vColor = inputs.color;
output.Position = camera.uProjectionMatrix * vec4<f32>(inputs.position, 1.0);
HOOK_afterVertex();
return output;
}
`;
const materialFragmentShader = `
struct FragmentInput {
@location(0) vNormal: vec3<f32>,
@location(1) vTexCoord: vec2<f32>,
@location(2) vViewPosition: vec3<f32>,
@location(4) vColor: vec4<f32>,
};
${uniforms$3}
@group(0) @binding(0) var<uniform> material: MaterialUniforms;
@group(0) @binding(1) var<uniform> lighting: LightingUniforms;
@group(0) @binding(2) var uSampler: texture_2d<f32>;
@group(0) @binding(3) var uSampler_sampler: sampler;
@group(0) @binding(4) var environmentMapDiffused: texture_2d<f32>;
@group(0) @binding(5) var environmentMapDiffused_sampler: sampler;
@group(0) @binding(6) var environmentMapSpecular: texture_2d<f32>;
@group(0) @binding(7) var environmentMapSpecular_sampler: sampler;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
struct ColorComponents {
baseColor: vec3<f32>,
opacity: f32,
ambientColor: vec3<f32>,
specularColor: vec3<f32>,
diffuse: vec3<f32>,
ambient: vec3<f32>,
specular: vec3<f32>,
emissive: vec3<f32>,
}
struct Inputs {
normal: vec3<f32>,
texCoord: vec2<f32>,
ambientLight: vec3<f32>,
ambientMaterial: vec3<f32>,
specularMaterial: vec3<f32>,
emissiveMaterial: vec3<f32>,
color: vec4<f32>,
shininess: f32,
metalness: f32,
}
struct LightResult {
diffuse: vec3<f32>,
specular: vec3<f32>,
}
struct LightIntensityResult {
diffuse: f32,
specular: f32,
}
const specularFactor = 2.0;
const diffuseFactor = 0.73;
const PI = 3.14159265359;
fn mapTextureToNormal(v: vec3<f32>) -> vec2<f32> {
// x = r sin(phi) cos(theta)
// y = r cos(phi)
// z = r sin(phi) sin(theta)
let phi = acos(v.y);
// if phi is 0, then there are no x, z components
var theta = 0.0;
// else
theta = acos(v.x / sin(phi));
let sinTheta = v.z / sin(phi);
if (sinTheta < 0.0) {
// Turn it into -theta, but in the 0-2PI range
theta = 2.0 * PI - theta;
}
theta = theta / (2.0 * PI);
let phiNorm = phi / PI;
let angles = vec2<f32>(fract(theta + 0.25), 1.0 - phiNorm);
return angles;
}
fn calculateImageDiffuse(vNormal: vec3<f32>, vViewPosition: vec3<f32>, metallic: f32) -> vec3<f32> {
// make 2 seperate builds
let worldCameraPosition = vec3<f32>(0.0, 0.0, 0.0); // hardcoded world camera position
let worldNormal = normalize(vNormal * camera.uCameraNormalMatrix);
let newTexCoord = mapTextureToNormal(worldNormal);
let texture = textureSample(environmentMapDiffused, environmentMapDiffused_sampler, newTexCoord);
// this is to make the darker sections more dark
// png and jpg usually flatten the brightness so it is to reverse that
return mix(smoothstep(vec3<f32>(0.0), vec3<f32>(1.0), texture.xyz), vec3<f32>(0.0), metallic);
}
fn calculateImageSpecular(vNormal: vec3<f32>, vViewPosition: vec3<f32>, shininess: f32, metallic: f32) -> vec3<f32> {
let worldCameraPosition = vec3<f32>(0.0, 0.0, 0.0);
let worldNormal = normalize(vNormal);
let lightDirection = normalize(vViewPosition - worldCameraPosition);
let R = reflect(lightDirection, worldNormal) * camera.uCameraNormalMatrix;
let newTexCoord = mapTextureToNormal(R);
// In p5js the range of shininess is >= 1,
// Therefore roughness range will be ([0,1]*8)*20 or [0, 160]
// The factor of 8 is because currently the getSpecularTexture
// only calculated 8 different levels of roughness
// The factor of 20 is just to spread up this range so that,
// [1, max] of shininess is converted to [0,160] of roughness
let roughness = 20.0 / shininess;
let outColor = textureSampleLevel(environmentMapSpecular, environmentMapSpecular_sampler, newTexCoord, roughness * 8.0 - 1.);
// this is to make the darker sections more dark
// png and jpg usually flatten the brightness so it is to reverse that
return mix(
pow(outColor.xyz, vec3<f32>(10.0)),
pow(outColor.xyz, vec3<f32>(1.2)),
metallic
);
}
fn phongSpecular(
lightDirection: vec3<f32>,
viewDirection: vec3<f32>,
surfaceNormal: vec3<f32>,
shininess: f32
) -> f32 {
let R = reflect(lightDirection, surfaceNormal);
return pow(max(0.0, dot(R, viewDirection)), shininess);
}
fn lambertDiffuse(lightDirection: vec3<f32>, surfaceNormal: vec3<f32>) -> f32 {
return max(0.0, dot(-lightDirection, surfaceNormal));
}
fn singleLight(
viewDirection: vec3<f32>,
normal: vec3<f32>,
lightVector: vec3<f32>,
shininess: f32,
metallic: f32
) -> LightIntensityResult {
let lightDir = normalize(lightVector);
let specularIntensity = mix(1.0, 0.4, metallic);
let diffuseIntensity = mix(1.0, 0.1, metallic);
let diffuse = lambertDiffuse(lightDir, normal) * diffuseIntensity;
let specular = select(
0.,
phongSpecular(lightDir, viewDirection, normal, shininess) * specularIntensity,
material.uSpecular == 1
);
return LightIntensityResult(diffuse, specular);
}
fn totalLight(
modelPosition: vec3<f32>,
normal: vec3<f32>,
shininess: f32,
metallic: f32
) -> LightResult {
var totalSpecular = vec3<f32>(0.0, 0.0, 0.0);
var totalDiffuse = vec3<f32>(0.0, 0.0, 0.0);
if (lighting.uUseLighting == 0) {
return LightResult(vec3<f32>(1.0, 1.0, 1.0), totalSpecular);
}
let viewDirection = normalize(-modelPosition);
for (var j = 0; j < 5; j++) {
if (j < lighting.uDirectionalLightCount) {
let lightVector = (camera.uViewMatrix * vec4<f32>(
lighting.uLightingDirection[j],
0.0
)).xyz;
let lightColor = lighting.uDirectionalDiffuseColors[j];
let specularColor = lighting.uDirectionalSpecularColors[j];
let result = singleLight(viewDirection, normal, lightVector, shininess, metallic);
totalDiffuse += result.diffuse * lightColor;
totalSpecular += result.specular * specularColor;
}
if (j < lighting.uPointLightCount) {
let lightPosition = (camera.uViewMatrix * vec4<f32>(
lighting.uPointLightLocation[j],
1.0
)).xyz;
let lightVector = modelPosition - lightPosition;
let lightDistance = length(lightVector);
let lightFalloff = 1.0 / (
lighting.uConstantAttenuation +
lightDistance * lighting.uLinearAttenuation +
lightDistance * lightDistance * lighting.uQuadraticAttenuation
);
let lightColor = lighting.uPointLightDiffuseColors[j] * lightFalloff;
let specularColor = lighting.uPointLightSpecularColors[j] * lightFalloff;
let result = singleLight(viewDirection, normal, lightVector, shininess, metallic);
totalDiffuse += result.diffuse * lightColor;
totalSpecular += result.specular * specularColor;
}
if (j < lighting.uSpotLightCount) {
let lightPosition = (camera.uViewMatrix * vec4<f32>(
lighting.uSpotLightLocation[j],
1.0
)).xyz;
let lightVector = modelPosition - lightPosition;
let lightDistance = length(lightVector);
var lightFalloff = 1.0 / (
lighting.uConstantAttenuation +
lightDistance * lighting.uLinearAttenuation +
lightDistance * lightDistance * lighting.uQuadraticAttenuation
);
let lightDirection = (camera.uViewMatrix * vec4<f32>(
lighting.uSpotLightDirection[j],
0.0
)).xyz;
let spotDot = dot(normalize(lightVector), normalize(lightDirection));
let spotFalloff = select(
0.0,
pow(spotDot, lighting.uSpotLightConc[j]),
spotDot < lighting.uSpotLightAngle[j]
);
lightFalloff *= spotFalloff;
let lightColor = lighting.uSpotLightDiffuseColors[j];
let specularColor = lighting.uSpotLightSpecularColors[j];
let result = singleLight(viewDirection, normal, lightVector, shininess, metallic);
totalDiffuse += result.diffuse * lightColor;
totalSpecular += result.specular * specularColor;
}
}
// Image light contribution
if (lighting.uUseImageLight != 0) {
totalDiffuse += calculateImageDiffuse(normal, modelPosition, metallic);
totalSpecular += calculateImageSpecular(normal, modelPosition, shininess, metallic);
}
return LightResult(
totalDiffuse * diffuseFactor,
totalSpecular * specularFactor
);
}
@fragment
fn main(input: FragmentInput) -> @location(0) vec4<f32> {
HOOK_beforeFragment();
let color = select(
input.vColor,
textureSample(uSampler, uSampler_sampler, input.vTexCoord) * (material.uTint/255.0),
material.isTexture == 1
); // TODO: check isTexture and apply tint
var inputs = Inputs(
normalize(input.vNormal),
input.vTexCoord,
material.uAmbientColor,
select(color.rgb, material.uAmbientMatColor.rgb, material.uHasSetAmbient == 1),
material.uSpecularMatColor.rgb,
material.uEmissiveMatColor.rgb,
color,
material.uShininess,
material.uMetallic
);
inputs = HOOK_getPixelInputs(inputs);
let light = totalLight(
input.vViewPosition,
inputs.normal,
inputs.shininess,
inputs.metalness
);
let baseColor = inputs.color;
let components = ColorComponents(
baseColor.rgb,
baseColor.a,
inputs.ambientMaterial,
inputs.specularMaterial,
light.diffuse,
inputs.ambientLight,
light.specular,
inputs.emissiveMaterial
);
var outColor = HOOK_getFinalColor(
HOOK_combineColors(components), input.vTexCoord
);
outColor = vec4<f32>(outColor.rgb * outColor.a, outColor.a);
HOOK_afterFragment();
return outColor;
}
`;
const uniforms$2 = `
// Group 0: Font Properties
struct FontUniforms {
uStrokeImageSize: vec2<i32>,
uCellsImageSize: vec2<i32>,
uGridImageSize: vec2<i32>,
uGridOffset: vec2<i32>,
uGridSize: vec2<i32>,
uGlyphRect: vec4<f32>,
uGlyphOffset: f32,
uMaterialColor: vec4<f32>,
}
// Group 1: Model Transform
struct ModelUniforms {
uModelViewMatrix: mat4x4<f32>,
}
// Group 2: Camera and Projection
struct CameraUniforms {
uProjectionMatrix: mat4x4<f32>,
}
`;
const fontVertexShader = `
struct VertexInput {
@location(0) aPosition: vec3<f32>,
@location(1) aTexCoord: vec2<f32>,
};
struct VertexOutput {
@builtin(position) Position: vec4<f32>,
@location(0) vTexCoord: vec2<f32>,
};
${uniforms$2}
@group(0) @binding(0) var<uniform> font: FontUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
@vertex
fn main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
var positionVec4 = vec4<f32>(input.aPosition, 1.0);
// scale by the size of the glyph's rectangle
positionVec4.x = positionVec4.x * (font.uGlyphRect.z - font.uGlyphRect.x);
positionVec4.y = positionVec4.y * (font.uGlyphRect.w - font.uGlyphRect.y);
// Expand glyph bounding boxes by 1px on each side to give a bit of room
// for antialiasing
let newOrigin = (model.uModelViewMatrix * vec4<f32>(0.0, 0.0, 0.0, 1.0)).xyz;
let newDX = (model.uModelViewMatrix * vec4<f32>(1.0, 0.0, 0.0, 1.0)).xyz;
let newDY = (model.uModelViewMatrix * vec4<f32>(0.0, 1.0, 0.0, 1.0)).xyz;
let pixelScale = vec2<f32>(
1.0 / length(newOrigin - newDX),
1.0 / length(newOrigin - newDY)
);
let offset = pixelScale * normalize(input.aTexCoord - vec2<f32>(0.5, 0.5));
let textureOffset = offset * (1.0 / vec2<f32>(
font.uGlyphRect.z - font.uGlyphRect.x,
font.uGlyphRect.w - font.uGlyphRect.y
));
// move to the corner of the glyph
positionVec4.x = positionVec4.x + font.uGlyphRect.x;
positionVec4.y = positionVec4.y + font.uGlyphRect.y;
// move to the letter's line offset
positionVec4.x = positionVec4.x + font.uGlyphOffset;
positionVec4.x = positionVec4.x + offset.x;
positionVec4.y = positionVec4.y + offset.y;
output.Position = camera.uProjectionMatrix * model.uModelViewMatrix * positionVec4;
output.vTexCoord = input.aTexCoord + textureOffset;
return output;
}
`;
const fontFragmentShader = `
struct FragmentInput {
@location(0) vTexCoord: vec2<f32>,
};
${uniforms$2}
@group(0) @binding(0) var<uniform> font: FontUniforms;
@group(0) @binding(1) var uSamplerStrokes: texture_2d<f32>;
@group(0) @binding(2) var uSamplerStrokes_sampler: sampler;
@group(0) @binding(3) var uSamplerRowStrokes: texture_2d<f32>;
@group(0) @binding(4) var uSamplerRowStrokes_sampler: sampler;
@group(0) @binding(5) var uSamplerRows: texture_2d<f32>;
@group(0) @binding(6) var uSamplerRows_sampler: sampler;
@group(0) @binding(7) var uSamplerColStrokes: texture_2d<f32>;
@group(0) @binding(8) var uSamplerColStrokes_sampler: sampler;
@group(0) @binding(9) var uSamplerCols: texture_2d<f32>;
@group(0) @binding(10) var uSamplerCols_sampler: sampler;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
// some helper functions
fn ROUND_f32(v: f32) -> i32 { return i32(floor(v + 0.5)); }
fn ROUND_vec2(v: vec2<f32>) -> vec2<i32> { return vec2<i32>(floor(v + 0.5)); }
fn saturate_f32(v: f32) -> f32 { return clamp(v, 0.0, 1.0); }
fn saturate_vec2(v: vec2<f32>) -> vec2<f32> { return clamp(v, vec2<f32>(0.0), vec2<f32>(1.0)); }
fn mul_f32_i32(v1: f32, v2: i32) -> i32 {
return i32(floor(v1 * f32(v2)));
}
fn mul_vec2_ivec2(v1: vec2<f32>, v2: vec2<i32>) -> vec2<i32> {
return vec2<i32>(floor(v1 * vec2<f32>(v2) + 0.5));
}
// unpack a 16-bit integer from a float vec2
fn getInt16(v: vec2<f32>) -> i32 {
let iv = ROUND_vec2(v * 255.0);
return iv.x * 128 + iv.y;
}
const minDistance: f32 = 1.0/8192.0;
const hardness: f32 = 1.05; // amount of antialias
// the maximum number of curves in a glyph
const N: i32 = 250;
// retrieves an indexed pixel from a texture
fn getTexel(texture: texture_2d<f32>, samp: sampler, pos: i32, size: vec2<i32>) -> vec4<f32> {
let width = size.x;
let x = pos % width;
let y = pos / width;
return textureLoad(texture, vec2<i32>(x, y), 0);
}
fn calculateCrossings(p0: vec2<f32>, p1: vec2<f32>, p2: vec2<f32>, vTexCoord: vec2<f32>, pixelScale: vec2<f32>) -> array<vec2<f32>, 2> {
// get the coefficients of the quadratic in t
var a = p0 - p1 * 2.0 + p2;
var b = p0 - p1;
a = vec2<f32>(
select(a.x, sign(a.x) * 1e-6, abs(a.x) < 1e-6),
select(a.y, sign(a.y) * 1e-6, abs(a.y) < 1e-6)
);
b = vec2<f32>(
select(b.x, sign(b.x) * 1e-6, abs(b.x) < 1e-6),
select(b.y, sign(b.y) * 1e-6, abs(b.y) < 1e-6)
);
let c = p0 - vTexCoord;
// found out which values of 't' it crosses the axes
let surd = sqrt(max(vec2<f32>(0.0), b * b - a * c));
let t1 = ((b - surd) / a).yx;
let t2 = ((b + surd) / a).yx;
// approximate straight lines to avoid rounding errors
var t1_fixed = t1;
var t2_fixed = t2;
if (abs(a.y) < 0.001) {
t1_fixed.x = c.y / (2.0 * b.y);
t2_fixed.x = c.y / (2.0 * b.y);
}
if (abs(a.x) < 0.001) {
t1_fixed.y = c.x / (2.0 * b.x);
t2_fixed.y = c.x / (2.0 * b.x);
}
// plug into quadratic formula to find the coordinates of the crossings
let C1 = ((a * t1_fixed - b * 2.0) * t1_fixed + c) * pixelScale;
let C2 = ((a * t2_fixed - b * 2.0) * t2_fixed + c) * pixelScale;
return array<vec2<f32>, 2>(C1, C2);
}
fn coverageX(p0: vec2<f32>, p1: vec2<f32>, p2: vec2<f32>, vTexCoord: vec2<f32>, pixelScale: vec2<f32>, coverage: ptr<function, vec2<f32>>, weight: ptr<function, vec2<f32>>) {
let crossings = calculateCrossings(p0, p1, p2, vTexCoord, pixelScale);
let C1 = crossings[0];
let C2 = crossings[1];
// determine on which side of the x-axis the points lie
let y0 = p0.y > vTexCoord.y;
let y1 = p1.y > vTexCoord.y;
let y2 = p2.y > vTexCoord.y;
// could we be under the curve (after t1)?
if ((y1 && !y2) || (!y1 && y0)) {
// add the coverage for t1
(*coverage).x = (*coverage).x + saturate_f32(C1.x + 0.5);
// calculate the anti-aliasing for t1
(*weight).x = min((*weight).x, abs(C1.x));
}
// are we outside the curve (after t2)?
if ((y1 && !y0) || (!y1 && y2)) {
// subtract the coverage for t2
(*coverage).x = (*coverage).x - saturate_f32(C2.x + 0.5);
// calculate the anti-aliasing for t2
(*weight).x = min((*weight).x, abs(C2.x));
}
}
// this is essentially the same as coverageX, but with the axes swapped
fn coverageY(p0: vec2<f32>, p1: vec2<f32>, p2: vec2<f32>, vTexCoord: vec2<f32>, pixelScale: vec2<f32>, coverage: ptr<function, vec2<f32>>, weight: ptr<function, vec2<f32>>) {
let crossings = calculateCrossings(p0, p1, p2, vTexCoord, pixelScale);
let C1 = crossings[0];
let C2 = crossings[1];
let x0 = p0.x > vTexCoord.x;
let x1 = p1.x > vTexCoord.x;
let x2 = p2.x > vTexCoord.x;
if ((x1 && !x2) || (!x1 && x0)) {
(*coverage).y = (*coverage).y - saturate_f32(C1.y + 0.5);
weight.y = min(weight.y, abs(C1.y));
}
if ((x1 && !x0) || (!x1 && x2)) {
(*coverage).y = (*coverage).y + saturate_f32(C2.y + 0.5);
(*weight).y = min((*weight).y, abs(C2.y));
}
}
@fragment
fn main(input: FragmentInput) -> @location(0) vec4<f32> {
// var pixelScale: vec2<f32>;
var coverage: vec2<f32> = vec2<f32>(0.0);
var weight: vec2<f32> = vec2<f32>(0.5);
let pixelScale = hardness / fwidth(input.vTexCoord);
// which grid cell is this pixel in?
let gridCoord = vec2<i32>(floor(input.vTexCoord * vec2<f32>(font.uGridSize)));
// intersect curves in this row
{
// the index into the row info bitmap
let rowIndex = gridCoord.y + font.uGridOffset.y;
// fetch the info texel
let rowInfo = getTexel(uSamplerRows, uSamplerRows_sampler, rowIndex, font.uGridImageSize);
// unpack the rowInfo
let rowStrokeIndex = getInt16(rowInfo.xy);
let rowStrokeCount = getInt16(rowInfo.zw);
for (var iRowStroke = 0; iRowStroke < N; iRowStroke = iRowStroke + 1) {
if (iRowStroke >= rowStrokeCount) {
break;
}
// each stroke is made up of 3 points: the start and control point
// and the start of the next curve.
// fetch the indices of this pair of strokes:
let strokeIndices = getTexel(uSamplerRowStrokes, uSamplerRowStrokes_sampler, rowStrokeIndex + iRowStroke, font.uCellsImageSize);
// unpack the stroke index
let strokePos = getInt16(strokeIndices.xy);
// fetch the two strokes
let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, font.uStrokeImageSize);
let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, font.uStrokeImageSize);
// calculate the coverage
coverageX(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight);
}
}
// intersect curves in this column
{
let colIndex = gridCoord.x + font.uGridOffset.x;
let colInfo = getTexel(uSamplerCols, uSamplerCols_sampler, colIndex, font.uGridImageSize);
let colStrokeIndex = getInt16(colInfo.xy);
let colStrokeCount = getInt16(colInfo.zw);
for (var iColStroke = 0; iColStroke < N; iColStroke = iColStroke + 1) {
if (iColStroke >= colStrokeCount) {
break;
}
let strokeIndices = getTexel(uSamplerColStrokes, uSamplerColStrokes_sampler, colStrokeIndex + iColStroke, font.uCellsImageSize);
let strokePos = getInt16(strokeIndices.xy);
let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, font.uStrokeImageSize);
let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, font.uStrokeImageSize);
coverageY(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight);
}
}
weight = saturate_vec2(vec2<f32>(1.0) - weight * 2.0);
let distance = max(weight.x + weight.y, minDistance); // manhattan approx.
let antialias = abs(dot(coverage, weight) / distance);
let cover = min(abs(coverage.x), abs(coverage.y));
var outColor = vec4<f32>(font.uMaterialColor.rgb, 1.0) * font.uMaterialColor.a;
outColor = outColor * saturate_f32(max(antialias, cover));
return outColor;
}
`;
const uniforms$1 = `
struct Uniforms {
uModelViewMatrix: mat4x4<f32>,
uProjectionMatrix: mat4x4<f32>,
};
`;
const blitVertexShader = `
struct VertexInput {
@location(0) aPosition: vec3<f32>,
@location(1) aNormal: vec3<f32>,
@location(2) aTexCoord: vec2<f32>,
@location(3) aVertexColor: vec4<f32>,
};
struct VertexOutput {
@builtin(position) Position: vec4<f32>,
@location(0) vTexCoord: vec2<f32>,
};
${uniforms$1}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@vertex
fn main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.vTexCoord = input.aTexCoord;
let positionVec4 = vec4<f32>(input.aPosition, 1.0);
output.Position = uniforms.uProjectionMatrix * uniforms.uModelViewMatrix * positionVec4;
return output;
}
`;
const blitFragmentShader = `
struct FragmentInput {
@location(0) vTexCoord: vec2<f32>,
};
${uniforms$1}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var uSampler: texture_2d<f32>;
@group(0) @binding(2) var uSampler_sampler: sampler;
@fragment
fn main(input: FragmentInput) -> @location(0) vec4<f32> {
return textureSample(uSampler, uSampler_sampler, input.vTexCoord);
}
`;
// Based on https://github.com/stegu/webgl-noise/blob/22434e04d7753f7e949e8d724ab3da2864c17a0f/src/noise3D.glsl
// MIT licensed, adapted for p5.strands and converted to WGSL
var noiseWGSL = `fn mod289Vec3(x: vec3<f32>) -> vec3<f32> {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
fn mod289Vec4(x: vec4<f32>) -> vec4<f32> {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
fn permute(x: vec4<f32>) -> vec4<f32> {
return mod289Vec4(((x*34.0)+10.0)*x);
}
fn taylorInvSqrt(r: vec4<f32>) -> vec4<f32> {
return vec4<f32>(1.79284291400159) - vec4<f32>(0.85373472095314) * r;
}
fn baseNoise(v: vec3<f32>) -> f32 {
let C = vec2<f32>(1.0/6.0, 1.0/3.0);
let D = vec4<f32>(0.0, 0.5, 1.0, 2.0);
// First corner
var i = floor(v + dot(v, C.yyy));
let x0 = v - i + dot(i, C.xxx);
// Other corners
let g = step(x0.yzx, x0.xyz);
let l = vec3<f32>(1.0) - g;
let i1 = min(g.xyz, l.zxy);
let i2 = max(g.xyz, l.zxy);
// x0 = x0 - 0.0 + 0.0 * C.xxx;
// x1 = x0 - i1 + 1.0 * C.xxx;
// x2 = x0 - i2 + 2.0 * C.xxx;
// x3 = x0 - 1.0 + 3.0 * C.xxx;
let x1 = x0 - i1 + C.xxx;
let x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
let x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
// Permutations
i = mod289Vec3(i);
let p = permute( permute( permute(
i.z + vec4<f32>(0.0, i1.z, i2.z, 1.0 ))
+ i.y + vec4<f32>(0.0, i1.y, i2.y, 1.0 ))
+ i.x + vec4<f32>(0.0, i1.x, i2.x, 1.0 ));
// Gradients: 7x7 points over a square, mapped onto an octahedron.
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
let n_ = 0.142857142857; // 1.0/7.0
let ns = n_ * D.wyz - D.xzx;
let j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
let x_ = floor(j * ns.z);
let y_ = floor(j - 7.0 * x_ ); // mod(j,N)
let x = x_ *ns.x + ns.yyyy;
let y = y_ *ns.x + ns.yyyy;
let h = vec4<f32>(1.0) - abs(x) - abs(y);
let b0 = vec4<f32>( x.xy, y.xy );
let b1 = vec4<f32>( x.zw, y.zw );
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
let s0 = floor(b0)*2.0 + vec4<f32>(1.0);
let s1 = floor(b1)*2.0 + vec4<f32>(1.0);
let sh = -step(h, vec4<f32>(0.0));
let a0 = b0.xzyw + s0.xzyw*sh.xxyy;
let a1 = b1.xzyw + s1.xzyw*sh.zzww;
let p0 = vec3<f32>(a0.xy, h.x);
let p1 = vec3<f32>(a0.zw, h.y);
let p2 = vec3<f32>(a1.xy, h.z);
let p3 = vec3<f32>(a1.zw, h.w);
//Normalise gradients
let norm = taylorInvSqrt(vec4<f32>(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
let p0_norm = p0 * norm.x;
let p1_norm = p1 * norm.y;
let p2_norm = p2 * norm.z;
let p3_norm = p3 * norm.w;
// Mix final noise value
var m = max(vec4<f32>(0.5) - vec4<f32>(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), vec4<f32>(0.0));
m = m * m;
return 105.0 * dot( m*m, vec4<f32>( dot(p0_norm,x0), dot(p1_norm,x1),
dot(p2_norm,x2), dot(p3_norm,x3) ) );
}
fn noise(st: vec3<f32>, octaves: i32, ampFalloff: f32) -> f32 {
var result = 0.0;
var amplitude = 1.0;
var frequency = 1.0;
for (var i = 0; i < 8; i++) {
if (i >= octaves) { break; }
result += amplitude * baseNoise(st * frequency);
frequency *= 2.0;
amplitude *= ampFalloff;
}
return (result + 1.0) * 0.5;
}`;
// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
// α₂ = 1/φ₂² = 0.5698402910
// 1/φ = 0.6180339887 (golden ratio conjugate)
//
// Fragment shader version: pixelCoord is passed in from main via @builtin(position).
var randomWGSL = `
var<private> _p5_randomCallIndex: i32 = 0;
fn _p5_hash(p: vec3<f32>) -> f32 {
var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
p3 = p3 + dot(p3, p3.yxz + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
fn random(seed: f32, pixelCoord: vec2<f32>) -> f32 {
let callIndex = f32(_p5_randomCallIndex);
_p5_randomCallIndex = _p5_randomCallIndex + 1;
let s = fract(seed * 0.7548776662);
return _p5_hash(vec3<f32>(
pixelCoord.x + s,
pixelCoord.y + callIndex * 0.5698402910,
s + callIndex * 0.6180339887
));
}
`;
// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
// α₂ = 1/φ₂² = 0.5698402910
// 1/φ = 0.6180339887 (golden ratio conjugate)
//
// Vertex shader version: vertexId is passed in from main via @builtin(vertex_index).
var randomVertWGSL = `
var<private> _p5_randomCallIndex: i32 = 0;
fn _p5_hash(p: vec3<f32>) -> f32 {
var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
p3 = p3 + dot(p3, p3.yxz + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
fn random(seed: f32, vertexId: f32) -> f32 {
let callIndex = f32(_p5_randomCallIndex);
_p5_randomCallIndex = _p5_randomCallIndex + 1;
let s = fract(seed * 0.7548776662);
return _p5_hash(vec3<f32>(
vertexId + s,
vertexId * 0.5698402910 + callIndex * 0.6180339887,
s + callIndex * 0.7548776662
));
}
`;
// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
// α₂ = 1/φ₂² = 0.5698402910
// 1/φ = 0.6180339887 (golden ratio conjugate)
//
// Compute shader version: invocationId is passed in from main via @builtin(global_invocation_id).
var randomComputeWGSL = `
var<private> _p5_randomCallIndex: i32 = 0;
fn _p5_hash(p: vec3<f32>) -> f32 {
var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
p3 = p3 + dot(p3, p3.yxz + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
fn random(seed: f32, invocationId: vec3<u32>) -> f32 {
let id = vec3<f32>(invocationId);
let callIndex = f32(_p5_randomCallIndex);
_p5_randomCallIndex = _p5_randomCallIndex + 1;
let s = fract(seed * 0.7548776662);
return _p5_hash(vec3<f32>(
id.x + s,
id.y + callIndex * 0.5698402910,
id.z + s + callIndex * 0.6180339887
));
}
`;
function internalError(errorMessage) {
const prefixedMessage = `[p5.strands internal error]: ${errorMessage}`;
throw new Error(prefixedMessage);
}
function userError(errorType, errorMessage) {
const prefixedMessage = `[p5.strands ${errorType}]: ${errorMessage}`;
throw new Error(prefixedMessage);
}
function dimensionMismatchError(declaredDim,actualDim,varName){
userError(
'dimension mismatch',
`Cannot assign a value of dimension ${actualDim} to \`${varName}\`, which expects dimension ${declaredDim}.`
);
}
function getOrCreateNode(graph, node) {
// const key = getNodeKey(node);
// const existing = graph.cache.get(key);
// if (existing !== undefined) {
// return existing;
// } else {
const id = createNode(graph, node);
// graph.cache.set(key, id);
return id;
// }
}
function createNodeData(data = {}) {
const node = {
nodeType: data.nodeType ?? null,
baseType: data.baseType ?? null,
dimension: data.dimension ?? null,
opCode: data.opCode ?? null,
value: data.value ?? null,
identifier: data.identifier ?? null,
statementType: data.statementType ?? null,
swizzle: data.swizzle ?? null,
dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [],
usedBy: Array.isArray(data.usedBy) ? data.usedBy : [],
phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [],
};
validateNode(node);
return node;
}
function getNodeDataFromID(graph, id) {
return {
id,
nodeType: graph.nodeTypes[id],
opCode: graph.opCodes[id],
value: graph.values[id],
identifier: graph.identifiers[id],
dependsOn: graph.dependsOn[id],
usedBy: graph.usedBy[id],
phiBlocks: graph.phiBlocks[id],
dimension: graph.dimensions[id],
baseType: graph.baseTypes[id],
statementType: graph.statementTypes[id],
swizzle: graph.swizzles[id],
}
}
function extractNodeTypeInfo(dag, nodeID) {
return {
baseType: dag.baseTypes[nodeID],
dimension: dag.dimensions[nodeID],
priority: BasePriority[dag.baseTypes[nodeID]],
};
}
/////////////////////////////////
// Private functions
/////////////////////////////////
function createNode(graph, node) {
const id = graph.nextID++;
graph.nodeTypes[id] = node.nodeType;
graph.opCodes[id] = node.opCode;
graph.values[id] = node.value;
graph.identifiers[id] = node.identifier;
graph.dependsOn[id] = node.dependsOn.slice();
graph.usedBy[id] = node.usedBy;
graph.phiBlocks[id] = node.phiBlocks.slice();
graph.baseTypes[id] = node.baseType;
graph.dimensions[id] = node.dimension;
graph.statementTypes[id] = node.statementType;
graph.swizzles[id] = node.swizzle;
for (const dep of node.dependsOn) {
if (!Array.isArray(graph.usedBy[dep])) {
graph.usedBy[dep] = [];
}
graph.usedBy[dep].push(id);
}
return id;
}
function validateNode(node){
const nodeType = node.nodeType;
const requiredFields = NodeTypeRequiredFields[nodeType];
if (requiredFields.length === 2) {
internalError(`Required fields for node type '${NodeTypeToName[nodeType]}' not defined. Please add them to the utils.js file in p5.strands!`);
}
const missingFields = [];
for (const field of requiredFields) {
if (node[field] === null) {
missingFields.push(field);
}
}
if (node.dependsOn?.some(v => v === undefined)) {
throw new Error('Undefined dependency!');
}
if (missingFields.length > 0) {
internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`);
}
}
function recordInBasicBlock(graph, blockID, nodeID) {
if (nodeID === undefined) {
internalError('undefined nodeID in `recordInBasicBlock()`');
}
if (blockID === undefined) {
internalError('undefined blockID in `recordInBasicBlock()');
}
graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || [];
graph.blockInstructions[blockID].push(nodeID);
}
class StrandsNode {
constructor(id, dimension, strandsContext) {
this.id = id;
this.strandsContext = strandsContext;
this.dimension = dimension;
this.structProperties = null;
// Schema for struct storage buffers (set by uniformStorage when buffer has a struct layout).
// When set, buf.get(idx) returns a field proxy instead of a scalar StrandsNode.
this._schema = null;
this.isStrandsNode = true;
// Store original identifier for varying variables
const dag = this.strandsContext.dag;
const nodeData = getNodeDataFromID(dag, this.id);
if (nodeData && nodeData.identifier) {
this._originalIdentifier = nodeData.identifier;
}
if (nodeData) {
this._originalBaseType = nodeData.baseType;
this._originalDimension = nodeData.dimension;
}
}
withStructProperties(properties) {
this.structProperties = properties;
return this;
}
copy() {
return createStrandsNode(this.id, this.dimension, this.strandsContext);
}
typeInfo() {
return {
baseType: this._originalBaseType || BaseType.FLOAT,
dimension: this.dimension
};
}
bridge(value) {
const { dag, cfg } = this.strandsContext;
const orig = getNodeDataFromID(dag, this.id);
const baseType = orig?.baseType ?? BaseType.FLOAT;
let newValueID;
if (value?.isStrandsNode) {
newValueID = value.id;
} else {
const newVal = primitiveConstructorNode(
this.strandsContext,
{ baseType, dimension: this.dimension },
value
);
newValueID = newVal.id;
}
// For varying variables, we need both assignment generation AND a way to reference by identifier
if (this._originalIdentifier) {
const valueDim = value?.isStrandsNode
? value.dimension
: (Array.isArray(value) ? value.length : 1);
if (valueDim !== this._originalDimension && valueDim !== 1){
dimensionMismatchError(
this._originalDimension,
valueDim,
this._originalIdentifier
);
}
// Create a variable node for the target (the varying variable)
const { id: targetVarID } = variableNode(
this.strandsContext,
{ baseType: this._originalBaseType, dimension: this._originalDimension },
this._originalIdentifier
);
// Create assignment node for GLSL generation
const assignmentNode = createNodeData({
nodeType: NodeType.ASSIGNMENT,
dependsOn: [targetVarID, newValueID],
phiBlocks: []
});
const assignmentID = getOrCreateNode(dag, assignmentNode);
recordInBasicBlock(cfg, cfg.currentBlock, assignmentID);
// Simply update this node to be a variable node with the identifier
// This ensures it always generates the variable name in expressions
const variableNodeData = createNodeData({
nodeType: NodeType.VARIABLE,
baseType: this._originalBaseType,
dimension: this._originalDimension,
identifier: this._originalIdentifier
});
const variableID = getOrCreateNode(dag, variableNodeData);
this.id = variableID; // Point to the variable node for expression generation
} else {
this.id = newValueID; // For non-varying variables, just update to new value
}
return this;
}
bridgeSwizzle(swizzlePattern, value) {
const { dag, cfg } = this.strandsContext;
const orig = getNodeDataFromID(dag, this.id);
const baseType = orig?.baseType ?? BaseType.FLOAT;
let newValueID;
if (value?.isStrandsNode) {
newValueID = value.id;
} else {
const newVal = primitiveConstructorNode(
this.strandsContext,
{ baseType, dimension: this.dimension },
value
);
newValueID = newVal.id;
}
// For varying variables, create swizzle assignment
if (this._originalIdentifier) {
const valueDim = value?.isStrandsNode
? value.dimension
: (Array.isArray(value) ? value.length : 1);
if (valueDim !== swizzlePattern.length && valueDim !== 1){
dimensionMismatchError(
swizzlePattern.length,
valueDim,
`${this._originalIdentifier}.${swizzlePattern}`
);
}
// Create a variable node for the target with swizzle
const { id: targetVarID } = variableNode(
this.strandsContext,
{ baseType: this._originalBaseType, dimension: this._originalDimension },
this._originalIdentifier
);
// Create a swizzle node for the target (myVarying.xyz)
const swizzleNode = createNodeData({
nodeType: NodeType.OPERATION,
opCode: OpCode.Unary.SWIZZLE,
baseType: this._originalBaseType,
dimension: swizzlePattern.length, // xyz = 3, xy = 2, etc.
swizzle: swizzlePattern,
dependsOn: [targetVarID]
});
const swizzleID = getOrCreateNode(dag, swizzleNode);
// Create assignment node: myVarying.xyz = value
const assignmentNode = createNodeData({
nodeType: NodeType.ASSIGNMENT,
dependsOn: [swizzleID, newValueID],
phiBlocks: []
});
const assignmentID = getOrCreateNode(dag, assignmentNode);
recordInBasicBlock(cfg, cfg.currentBlock, assignmentID);
// Simply update this node to be a variable node with the identifier
// This ensures it always generates the variable name in expressions
const variableNodeData = createNodeData({
nodeType: NodeType.VARIABLE,
baseType: this._originalBaseType,
dimension: this._originalDimension,
identifier: this._originalIdentifier
});
const variableID = getOrCreateNode(dag, variableNodeData);
this.id = variableID; // Point to the variable node, not the assignment node
} else {
this.id = newValueID; // For non-varying variables, just update to new value
}
return this;
}
getValue() {
if (this._originalIdentifier) {
const { id, dimension } = variableNode(
this.strandsContext,
{ baseType: this._originalBaseType, dimension: this._originalDimension },
this._originalIdentifier
);
return createStrandsNode(id, dimension, this.strandsContext);
}
return this;
}
get(index) {
const nodeData = getNodeDataFromID(this.strandsContext.dag, this.id);
// Validate baseType is 'storage'
// For struct storage buffers, return a proxy with per-field getters/setters
if (nodeData.baseType === 'storage' && this._schema) {
return createStructArrayElementProxy(this.strandsContext, this, index, this._schema);
}
// Create array access node for storage and non-storage (vector) access
const { id, dimension } = arrayAccessNode(
this.strandsContext,
this,
index);
return createStrandsNode(id, dimension, this.strandsContext);
}
set(index, value) {
// Validate baseType is 'storage' and has _originalIdentifier
const nodeData = getNodeDataFromID(this.strandsContext.dag, this.id);
if (nodeData.baseType !== 'storage') {
throw new Error('set() can only be used on storage buffers');
}
if (!this._originalIdentifier) {
throw new Error('set() can only be used on storage buffers with an identifier');
}
// If value is a plain object (struct literal), expand to per-field assignments
// e.g. buf[idx] = { position: pos, velocity: vel }
// becomes buf[idx].position = pos; buf[idx].velocity = vel;
if (value !== null && typeof value === 'object' && !value.isStrandsNode && this._schema) {
const proxy = createStructArrayElementProxy(this.strandsContext, this, index, this._schema);
for (const [fieldName, fieldValue] of Object.entries(value)) {
proxy[fieldName] = fieldValue;
}
return this;
}
// Create array assignment node: buffer.set(index, value) -> buffer[index] = value
// This creates an ASSIGNMENT node and records it in the CFG basic block
// CFG preserves sequential order, preventing reordering of assignments
arrayAssignmentNode(this.strandsContext, this, index, value);
// Return this for chaining
return this;
}
}
function createStrandsNode(id, dimension, strandsContext, onRebind) {
return new Proxy(
new StrandsNode(id, dimension, strandsContext),
swizzleTrap(id, dimension, strandsContext, onRebind)
);
}
// Need the .js extension because we also import this from a Node script.
// Try to keep this file minimal because of that.
// GLSL Built in functions
// https://docs.gl/el3/abs
const builtInGLSLFunctions = {
//////////// Trigonometry //////////
acos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
acosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
asin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
asinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
atan: [
{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
],
atanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
cos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
cosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
degrees: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
radians: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
sin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT , isp5Function: true}],
sinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
tan: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
tanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
////////// Mathematics //////////
abs: [
{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
{ params: [GenType.FLOAT], returnType: GenType.INT, isp5Function: true}
],
ceil: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
clamp: [
{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
{ params: [GenType.FLOAT,DataType.float1,DataType.float1], returnType: GenType.FLOAT, isp5Function: false},
{ params: [GenType.INT, GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: false},
{ params: [GenType.INT, DataType.int1, DataType.int1], returnType: GenType.INT, isp5Function: false},
],
dFdx: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
dFdy: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
exp: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
exp2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
floor: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
fma: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
fract: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
fwidth: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
inversesqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
// "isinf": [{}],
// "isnan": [{}],
log: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
log2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
max: [
{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
{ params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true},
{ params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true},
{ params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true},
],
min: [
{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
{ params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true},
{ params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true},
{ params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true},
],
mix: [
{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false},
{ params: [GenType.FLOAT, GenType.FLOAT, GenType.BOOL], returnType: GenType.FLOAT, isp5Function: false},
],
mod: [
{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
{ params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true},
],
// "modf": [{}],
pow: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
round: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
roundEven: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
sign: [
{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
{ params: [GenType.INT], returnType: GenType.INT, isp5Function: false},
],
smoothstep: [
{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
{ params: [ DataType.float1,DataType.float1, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
],
sqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
step: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
trunc: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
////////// Vector //////////
cross: [{ params: [DataType.float3, DataType.float3], returnType: DataType.float3, isp5Function: true}],
distance: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}],
dot: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}],
equal: [
{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false},
{ params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false},
{ params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false},
],
faceforward: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
length: [{ params: [GenType.FLOAT], returnType:DataType.float1, isp5Function: false}],
normalize: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
notEqual: [
{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false},
{ params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false},
{ params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false},
],
reflect: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
refract: [{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}],
};
const strandsBuiltinFunctions = {
...builtInGLSLFunctions,
};
//////////////////////////////////////////////
// Builders for node graphs
//////////////////////////////////////////////
function scalarLiteralNode(strandsContext, typeInfo, value) {
const { cfg, dag } = strandsContext;
let { dimension, baseType } = typeInfo;
if (dimension !== 1) {
internalError('Created a scalar literal node with dimension > 1.');
}
const nodeData = createNodeData({
nodeType: NodeType.LITERAL,
dimension,
baseType,
value
});
const id = getOrCreateNode(dag, nodeData);
recordInBasicBlock(cfg, cfg.currentBlock, id);
return { id, dimension };
}
function variableNode(strandsContext, typeInfo, identifier) {
const { cfg, dag } = strandsContext;
const { dimension, baseType } = typeInfo;
const nodeData = createNodeData({
nodeType: NodeType.VARIABLE,
dimension,
baseType,
identifier
});
const id = getOrCreateNode(dag, nodeData);
recordInBasicBlock(cfg, cfg.currentBlock, id);
return { id, dimension };
}
function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) {
const inputs = Array.isArray(dependsOn) ? dependsOn : [dependsOn];
const mappedDependencies = [];
let { dimension, baseType } = typeInfo;
const dag = strandsContext.dag;
let calculatedDimensions = 0;
let originalNodeID = null;
for (const dep of inputs.flat(Infinity)) {
if (dep && dep.isStrandsNode) {
const node = getNodeDataFromID(dag, dep.id);
originalNodeID = dep.id;
baseType = node.baseType;
if (node.opCode === OpCode.Nary.CONSTRUCTOR) {
for (const inner of node.dependsOn) {
mappedDependencies.push(inner);
}
} else {
mappedDependencies.push(dep.id);
}
calculatedDimensions += node.dimension;
continue;
}
else if (typeof dep === 'number') {
const { id, dimension } = scalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep);
mappedDependencies.push(id);
calculatedDimensions += dimension;
continue;
}
else if (typeof dep === 'boolean') {
// Handle boolean literals - convert to bool type
const { id, dimension } = scalarLiteralNode(strandsContext, { dimension: 1, baseType: BaseType.BOOL }, dep);
mappedDependencies.push(id);
calculatedDimensions += dimension;
// Update baseType to BOOL if it was inferred
if (baseType !== BaseType.BOOL) {
baseType = BaseType.BOOL;
}
continue;
}
else {
userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`);
}
}
if (dimension === null) {
dimension = calculatedDimensions;
} else if (dimension > calculatedDimensions && calculatedDimensions === 1) {
calculatedDimensions = dimension;
} else if(calculatedDimensions !== 1 && calculatedDimensions !== dimension) {
userError('type error', `You've tried to construct a ${baseType + dimension} with ${calculatedDimensions} components`);
}
const inferredTypeInfo = {
dimension,
baseType,
priority: BasePriority[baseType],
};
return { originalNodeID, mappedDependencies, inferredTypeInfo };
}
function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray) {
const nodeData = createNodeData({
nodeType: NodeType.OPERATION,
opCode: OpCode.Nary.CONSTRUCTOR,
dimension: typeInfo.dimension,
baseType: typeInfo.baseType,
dependsOn: strandsNodesArray
});
const id = getOrCreateNode(strandsContext.dag, nodeData);
return id;
}
function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) {
const cfg = strandsContext.cfg;
dependsOn = (Array.isArray(dependsOn) ? dependsOn : [dependsOn])
.flat(Infinity)
.map(a => {
if (
a.isStrandsNode &&
a.typeInfo().baseType === BaseType.INT &&
// TODO: handle ivec inputs instead of just int scalars
a.typeInfo().dimension === 1
) {
return castToFloat(strandsContext, a);
} else {
return a;
}
});
const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn);
const finalType = {
// We might have inferred a non numeric type. Currently this is
// just used for booleans. Maybe this needs to be something more robust
// if we ever want to support inference of e.g. int vectors?
baseType: inferredTypeInfo.baseType === BaseType.BOOL
? BaseType.BOOL
: typeInfo.baseType,
dimension: inferredTypeInfo.dimension
};
const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies);
if (typeInfo.baseType !== BaseType.DEFER) {
recordInBasicBlock(cfg, cfg.currentBlock, id);
}
return { id, dimension: finalType.dimension, components: mappedDependencies };
}
function castToFloat(strandsContext, dep) {
const { id, dimension } = functionCallNode(
strandsContext,
strandsContext.backend.getTypeName('float', dep.typeInfo().dimension),
[dep],
{
overloads: [{
params: [dep.typeInfo()],
returnType: {
...dep.typeInfo(),
baseType: BaseType.FLOAT,
},
}],
}
);
return createStrandsNode(id, dimension, strandsContext);
}
function functionCallNode(
strandsContext,
functionName,
rawUserArgs,
{ overloads: rawOverloads } = {},
) {
const { cfg, dag } = strandsContext;
const overloads = rawOverloads || strandsBuiltinFunctions[functionName];
const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDepsToIDs(strandsContext, DataType.defer, rawUserArg));
const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length);
if (matchingArgsCounts.length === 0) {
const argsLengthSet = new Set();
const argsLengthArr = [];
overloads.forEach((overload) => argsLengthSet.add(overload.params.length));
argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`));
const argsLengthStr = argsLengthArr.join(', or ');
userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`);
}
const isGeneric = (T) => T.dimension === null;
let bestOverload = null;
let bestScore = 0;
let inferredReturnType = null;
let inferredDimension = null;
for (const overload of matchingArgsCounts) {
let isValid = true;
let similarity = 0;
for (let i = 0; i < preprocessedArgs.length; i++) {
const preArg = preprocessedArgs[i];
const argType = preArg.inferredTypeInfo;
const expectedType = overload.params[i];
let dimension = expectedType.dimension;
if (isGeneric(expectedType)) {
if (inferredDimension === null || inferredDimension === 1) {
inferredDimension = argType.dimension;
}
if (inferredDimension !== argType.dimension &&
!(argType.dimension === 1 && inferredDimension >= 1)
) {
isValid = false;
}
dimension = inferredDimension;
}
else {
if (argType.dimension > dimension) {
isValid = false;
}
}
if (argType.baseType === expectedType.baseType) {
similarity += 2;
}
else if(expectedType.priority > argType.priority) {
similarity += 1;
}
}
if (isValid && (!bestOverload || similarity > bestScore)) {
bestOverload = overload;
bestScore = similarity;
inferredReturnType = {...overload.returnType };
if (isGeneric(inferredReturnType)) {
inferredReturnType.dimension = inferredDimension;
}
}
}
if (bestOverload === null) {
userError('parameter validation', `No matching overload for ${functionName} was found!`);
}
let dependsOn = [];
for (let i = 0; i < bestOverload.params.length; i++) {
const arg = preprocessedArgs[i];
const paramType = { ...bestOverload.params[i] };
if (isGeneric(paramType)) {
paramType.dimension = inferredDimension;
}
if (arg.originalNodeID && typeEquals(arg.inferredTypeInfo, paramType)) {
dependsOn.push(arg.originalNodeID);
}
else {
const castedArgID = constructTypeFromIDs(strandsContext, paramType, arg.mappedDependencies);
recordInBasicBlock(cfg, cfg.currentBlock, castedArgID);
dependsOn.push(castedArgID);
}
}
const nodeData = createNodeData({
nodeType: NodeType.OPERATION,
opCode: OpCode.Nary.FUNCTION_CALL,
identifier: functionName,
dependsOn,
baseType: inferredReturnType.baseType,
dimension: inferredReturnType.dimension
});
const id = getOrCreateNode(dag, nodeData);
recordInBasicBlock(cfg, cfg.currentBlock, id);
return { id, dimension: inferredReturnType.dimension };
}
function swizzleNode(strandsContext, parentNode, swizzle) {
const { dag, cfg } = strandsContext;
const baseType = dag.baseTypes[parentNode.id];
const nodeData = createNodeData({
nodeType: NodeType.OPERATION,
baseType,
dimension: swizzle.length,
opCode: OpCode.Unary.SWIZZLE,
dependsOn: [parentNode.id],
swizzle,
});
const id = getOrCreateNode(dag, nodeData);
recordInBasicBlock(cfg, cfg.currentBlock, id);
return { id, dimension: swizzle.length };
}
function swizzleTrap(id, dimension, strandsContext, onRebind) {
const swizzleSets = [
['x', 'y', 'z', 'w'],
['r', 'g', 'b', 'a'],
['s', 't', 'p', 'q']
].map(s => s.slice(0, dimension));
const trap = {
get(target, property, receiver) {
if (property in target) {
return Reflect.get(...arguments);
} else {
for (const set of swizzleSets) {
if ([...property.toString()].every(char => set.includes(char))) {
const swizzle = [...property].map(char => {
const index = set.indexOf(char);
return swizzleSets[0][index];
}).join('');
const node = swizzleNode(strandsContext, target, swizzle);
return createStrandsNode(node.id, node.dimension, strandsContext);
}
}
}
},
set(target, property, value, receiver) {
for (const swizzleSet of swizzleSets) {
const chars = [...property];
const valid =
chars.every(c => swizzleSet.includes(c)) &&
new Set(chars).size === chars.length &&
target.dimension >= chars.length;
if (!valid) continue;
const dim = target.dimension;
// lanes are the underlying values of the target vector
// e.g. lane 0 holds the value aliased by 'x', 'r', and 's'
// the lanes array is in the 'correct' order
const lanes = new Array(dim);
for (let i = 0; i < dim; i++) {
const { id, dimension } = swizzleNode(strandsContext, target, 'xyzw'[i]);
lanes[i] = createStrandsNode(id, dimension, strandsContext);
}
// The scalars array contains the individual components of the users values.
// This may not be the most efficient way, as we swizzle each component individually,
// so that .xyz becomes .x, .y, .z
let scalars = [];
if (value?.isStrandsNode) {
if (value.dimension === 1) {
scalars = Array(chars.length).fill(value);
} else if (value.dimension === chars.length) {
for (let k = 0; k < chars.length; k++) {
const { id, dimension } = swizzleNode(strandsContext, value, 'xyzw'[k]);
scalars.push(createStrandsNode(id, dimension, strandsContext));
}
} else {
dimensionMismatchError(
chars.length,
value.dimension,
`${target._originalIdentifier || 'value'}.${property}`
);
}
} else if (Array.isArray(value)) {
const flat = value.flat(Infinity);
if (flat.length === 1) {
scalars = Array(chars.length).fill(flat[0]);
} else if (flat.length === chars.length) {
scalars = flat;
} else {
userError('type error', `Swizzle assignment: RHS length ${flat.length} does not match ${chars.length}.`);
}
} else if (typeof value === 'number') {
scalars = Array(chars.length).fill(value);
} else {
userError('type error', `Unsupported RHS for swizzle assignment: ${value}`);
}
// The canonical index refers to the actual value's position in the vector lanes
// i.e. we are finding (3,2,1) from .zyx
// We set the correct value in the lanes array
for (let j = 0; j < chars.length; j++) {
const canonicalIndex = swizzleSet.indexOf(chars[j]);
lanes[canonicalIndex] = scalars[j];
}
const orig = getNodeDataFromID(strandsContext.dag, target.id);
const baseType = orig?.baseType ?? BaseType.FLOAT;
const { id: newID } = primitiveConstructorNode(
strandsContext,
{ baseType, dimension: dim },
lanes
);
target.id = newID;
// If we swizzle assign on a struct component i.e.
// inputs.position.rg = [1, 2]
// The onRebind callback will update the structs components so that it refers to the new values,
// and make a new ID for the struct with these new values
if (typeof onRebind === 'function') {
onRebind(newID);
}
return true;
}
return Reflect.set(...arguments);
}
};
return trap;
}
function arrayAccessNode(strandsContext, bufferNode, indexNode, accessMode) {
const { dag, cfg } = strandsContext;
// Ensure index is a StrandsNode
let index;
if (indexNode instanceof StrandsNode) {
index = indexNode;
} else {
const { id, dimension } = primitiveConstructorNode(
strandsContext,
{ baseType: BaseType.INT, dimension: 1 },
indexNode
);
index = createStrandsNode(id, dimension, strandsContext);
}
// Array access returns a single float
const nodeData = createNodeData({
nodeType: NodeType.OPERATION,
opCode: OpCode.Binary.ARRAY_ACCESS,
dependsOn: [bufferNode.id, index.id],
dimension: 1,
baseType: BaseType.FLOAT});
const id = getOrCreateNode(dag, nodeData);
recordInBasicBlock(cfg, cfg.currentBlock, id);
return { id, dimension: 1 };
}
function createStructArrayElementProxy(strandsContext, bufferNode, indexNode, schema) {
const { dag, cfg } = strandsContext;
// Ensure index is a StrandsNode
let index;
if (indexNode instanceof StrandsNode) {
index = indexNode;
} else {
const { id, dimension } = primitiveConstructorNode(
strandsContext,
{ baseType: BaseType.INT, dimension: 1 },
indexNode
);
index = createStrandsNode(id, dimension, strandsContext);
}
// Create a plain object with getters/setters for each struct field.
// When read, a field creates an ARRAY_ACCESS IR node with the field name encoded
// in the identifier slot. When written, an ASSIGNMENT IR node is recorded in the CFG.
const proxy = {};
for (const field of schema.fields) {
Object.defineProperty(proxy, field.name, {
get() {
// Encode field name in identifier so WGSL backend can emit buf[idx].field
const nodeData = createNodeData({
nodeType: NodeType.OPERATION,
opCode: OpCode.Binary.ARRAY_ACCESS,
dependsOn: [bufferNode.id, index.id],
dimension: field.dim,
baseType: BaseType.FLOAT,
identifier: field.name,
});
const id = getOrCreateNode(dag, nodeData);
recordInBasicBlock(cfg, cfg.currentBlock, id);
// When a swizzle assignment fires (e.g. buf[i].vel.y *= -1), onRebind
// receives the new vector ID and writes it back to the buffer field,
// equivalent to buf[i].vel = newVec.
const onRebind = (newFieldID) => {
const accessData = createNodeData({
nodeType: NodeType.OPERATION,
opCode: OpCode.Binary.ARRAY_ACCESS,
dependsOn: [bufferNode.id, index.id],
dimension: field.dim,
baseType: BaseType.FLOAT,
identifier: field.name,
});
const accessID = getOrCreateNode(dag, accessData);
const assignData = createNodeData({
nodeType: NodeType.ASSIGNMENT,
dependsOn: [accessID, newFieldID],
phiBlocks: [],
});
const assignID = getOrCreateNode(dag, assignData);
recordInBasicBlock(cfg, cfg.currentBlock, assignID);
};
return createStrandsNode(id, field.dim, strandsContext, onRebind);
},
set(val) {
// Create access node as assignment target (field name in identifier)
const accessData = createNodeData({
nodeType: NodeType.OPERATION,
opCode: OpCode.Binary.ARRAY_ACCESS,
dependsOn: [bufferNode.id, index.id],
dimension: field.dim,
baseType: BaseType.FLOAT,
identifier: field.name,
});
const accessID = getOrCreateNode(dag, accessData);
let valueID;
if (val?.isStrandsNode) {
valueID = val.id;
} else {
const { id } = primitiveConstructorNode(
strandsContext,
{ baseType: BaseType.FLOAT, dimension: field.dim },
val
);
valueID = id;
}
const assignData = createNodeData({
nodeType: NodeType.ASSIGNMENT,
dependsOn: [accessID, valueID],
phiBlocks: [],
});
const assignID = getOrCreateNode(dag, assignData);
recordInBasicBlock(cfg, cfg.currentBlock, assignID);
},
configurable: true,
});
}
return proxy;
}
function arrayAssignmentNode(strandsContext, bufferNode, indexNode, valueNode) {
const { dag, cfg } = strandsContext;
// Ensure index is a StrandsNode
let index;
if (indexNode instanceof StrandsNode) {
index = indexNode;
} else {
const { id, dimension } = primitiveConstructorNode(
strandsContext,
{ baseType: BaseType.INT, dimension: 1 },
indexNode
);
index = createStrandsNode(id, dimension, strandsContext);
}
// Ensure value is a StrandsNode
let value;
if (valueNode instanceof StrandsNode) {
value = valueNode;
} else {
const { id, dimension } = primitiveConstructorNode(
strandsContext,
{ baseType: BaseType.FLOAT, dimension: 1 },
valueNode
);
value = createStrandsNode(id, dimension, strandsContext);
}
// Create array access node as the assignment target
const arrayAccessData = createNodeData({
nodeType: NodeType.OPERATION,
opCode: OpCode.Binary.ARRAY_ACCESS,
dependsOn: [bufferNode.id, index.id],
dimension: 1,
baseType: BaseType.FLOAT
});
const arrayAccessID = getOrCreateNode(dag, arrayAccessData);
// Create assignment node: buffer[index] = value
const assignmentData = createNodeData({
nodeType: NodeType.ASSIGNMENT,
dependsOn: [arrayAccessID, value.id],
phiBlocks: []
});
const assignmentID = getOrCreateNode(dag, assignmentData);
// CRITICAL: Record in CFG to preserve sequential ordering
recordInBasicBlock(cfg, cfg.currentBlock, assignmentID);
return { id: assignmentID };
}
function shouldCreateTemp(dag, nodeID) {
const nodeType = dag.nodeTypes[nodeID];
if (nodeType !== NodeType.OPERATION) return false;
if (dag.baseTypes[nodeID] === BaseType.SAMPLER2D) return false;
const uses = dag.usedBy[nodeID] || [];
return uses.length > 1;
}
const TypeNames = {
'float1': 'f32',
'float2': 'vec2<f32>',
'float3': 'vec3<f32>',
'float4': 'vec4<f32>',
'int1': 'i32',
'int2': 'vec2<i32>',
'int3': 'vec3<i32>',
'int4': 'vec4<i32>',
'bool1': 'bool',
'bool2': 'vec2<bool>',
'bool3': 'vec3<bool>',
'bool4': 'vec4<bool>',
'mat2': 'mat2x2<f32>',
'mat3': 'mat3x3<f32>',
'mat4': 'mat4x4<f32>',
};
const cfgHandlers = {
[BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => {
const { dag, cfg } = strandsContext;
const instructions = cfg.blockInstructions[blockID] || [];
for (const nodeID of instructions) {
const nodeType = dag.nodeTypes[nodeID];
if (shouldCreateTemp(dag, nodeID)) {
const declaration = wgslBackend.generateDeclaration(generationContext, dag, nodeID);
generationContext.write(declaration);
}
if (nodeType === NodeType.STATEMENT) {
wgslBackend.generateStatement(generationContext, dag, nodeID);
}
if (nodeType === NodeType.ASSIGNMENT) {
wgslBackend.generateAssignment(generationContext, dag, nodeID);
generationContext.visitedNodes.add(nodeID);
}
}
},
[BlockType.BRANCH](blockID, strandsContext, generationContext) {
const { dag, cfg } = strandsContext;
// Find all phi nodes in this branch block and declare them
const blockInstructions = cfg.blockInstructions[blockID] || [];
for (const nodeID of blockInstructions) {
const node = getNodeDataFromID(dag, nodeID);
if (node.nodeType === NodeType.PHI) {
// Check if the phi node's first dependency already has a temp name
const dependsOn = node.dependsOn || [];
if (dependsOn.length > 0) {
const firstDependency = dependsOn[0];
const existingTempName = generationContext.tempNames[firstDependency];
if (existingTempName) {
// Reuse the existing temp name instead of creating a new one
generationContext.tempNames[nodeID] = existingTempName;
continue; // Skip declaration, just alias to existing variable
}
}
// Otherwise, create a new temp variable for the phi node
const tmp = `T${generationContext.nextTempID++}`;
generationContext.tempNames[nodeID] = tmp;
const T = extractNodeTypeInfo(dag, nodeID);
const typeName = wgslBackend.getTypeName(T.baseType, T.dimension);
// Initialize with default value - WGSL requires initialization
let defaultValue;
if (T.dimension === 1) {
defaultValue = this.defaultScalarValue(T.baseType);
} else {
// For vector types, use constructor with repeated scalar values
const scalarDefault = this.defaultScalarValue(T.baseType);
const components = Array(T.dimension).fill(scalarDefault).join(', ');
defaultValue = `${typeName}(${components})`;
}
generationContext.write(`var ${tmp}: ${typeName} = ${defaultValue};`);
}
}
this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
},
defaultScalarValue(baseType) {
if (baseType === BaseType.FLOAT) {
return '0.0';
} else if (baseType === BaseType.BOOL) {
return 'false';
} else {
return '0';
}
},
[BlockType.IF_COND](blockID, strandsContext, generationContext) {
const { dag, cfg } = strandsContext;
const conditionID = cfg.blockConditions[blockID];
const condExpr = wgslBackend.generateExpression(generationContext, dag, conditionID);
generationContext.write(`if (${condExpr})`);
this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
},
[BlockType.ELSE_COND](blockID, strandsContext, generationContext) {
generationContext.write(`else`);
this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
},
[BlockType.IF_BODY](blockID, strandsContext, generationContext) {
this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
this.assignPhiNodeValues(blockID, strandsContext, generationContext);
},
[BlockType.SCOPE_START](blockID, strandsContext, generationContext) {
generationContext.write(`{`);
generationContext.indent++;
},
[BlockType.SCOPE_END](blockID, strandsContext, generationContext) {
generationContext.indent--;
generationContext.write(`}`);
},
[BlockType.MERGE](blockID, strandsContext, generationContext) {
this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
},
[BlockType.FUNCTION](blockID, strandsContext, generationContext) {
this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
},
[BlockType.FOR](blockID, strandsContext, generationContext) {
const { dag, cfg } = strandsContext;
const instructions = cfg.blockInstructions[blockID] || [];
generationContext.write(`for (`);
// Set flag to suppress semicolon on the last statement
const originalSuppressSemicolon = generationContext.suppressSemicolon;
for (let i = 0; i < instructions.length; i++) {
const nodeID = instructions[i];
const node = getNodeDataFromID(dag, nodeID);
const isLast = i === instructions.length - 1;
// Suppress semicolon on the last statement
generationContext.suppressSemicolon = isLast;
if (shouldCreateTemp(dag, nodeID)) {
const declaration = wgslBackend.generateDeclaration(generationContext, dag, nodeID);
generationContext.write(declaration);
}
if (node.nodeType === NodeType.STATEMENT) {
wgslBackend.generateStatement(generationContext, dag, nodeID);
}
if (node.nodeType === NodeType.ASSIGNMENT) {
wgslBackend.generateAssignment(generationContext, dag, nodeID);
generationContext.visitedNodes.add(nodeID);
}
}
// Restore original flag
generationContext.suppressSemicolon = originalSuppressSemicolon;
generationContext.write(`)`);
},
assignPhiNodeValues(blockID, strandsContext, generationContext) {
const { dag, cfg } = strandsContext;
// Find all phi nodes that this block feeds into
const successors = cfg.outgoingEdges[blockID] || [];
for (const successorBlockID of successors) {
const instructions = cfg.blockInstructions[successorBlockID] || [];
for (const nodeID of instructions) {
const node = getNodeDataFromID(dag, nodeID);
if (node.nodeType === NodeType.PHI) {
// Find which input of this phi node corresponds to our block
const branchIndex = node.phiBlocks?.indexOf(blockID);
if (branchIndex !== -1 && branchIndex < node.dependsOn.length) {
const sourceNodeID = node.dependsOn[branchIndex];
const tempName = generationContext.tempNames[nodeID];
if (tempName && sourceNodeID !== null) {
const sourceExpr = wgslBackend.generateExpression(generationContext, dag, sourceNodeID);
generationContext.write(`${tempName} = ${sourceExpr};`);
}
}
}
}
}
},
};
const wgslBackend = {
hookEntry(hookType) {
const params = hookType.parameters.map((param) => {
// For struct types, use a raw prefix since we'll create a mutable copy
const paramName = param.type.properties ? `_p5_strands_raw_${param.name}` : `${HOOK_PARAM_PREFIX}${param.name}`;
return `${paramName}: ${param.type.typeName}`;
}).join(', ');
const firstLine = `(${params}) {`;
// Generate mutable copies for struct parameters
const mutableCopies = hookType.parameters
.filter(param => param.type.properties) // Only struct types
.map(param => ` var ${HOOK_PARAM_PREFIX}${param.name} = _p5_strands_raw_${param.name};`)
.join('\n');
return mutableCopies ? firstLine + '\n' + mutableCopies : firstLine;
},
addTextureBindingsToDeclarations(strandsContext) {
// Add texture and sampler bindings for sampler2D uniforms to both vertex and fragment declarations
if (!strandsContext.renderer || !strandsContext.baseShader) return;
let bindingIndex = strandsContext.renderer.getNextBindingIndex({
vert: strandsContext.baseShader._vertSrc,
frag: strandsContext.baseShader._fragSrc,
compute: strandsContext.baseShader._computeSrc,
});
for (const {name, typeInfo} of strandsContext.uniforms) {
if (typeInfo.baseType === 'sampler2D') {
const textureBinding = `@group(0) @binding(${bindingIndex}) var ${name}: texture_2d<f32>;`;
const samplerBinding = `@group(0) @binding(${bindingIndex + 1}) var ${name}_sampler: sampler;`;
strandsContext.vertexDeclarations.add(textureBinding);
strandsContext.vertexDeclarations.add(samplerBinding);
strandsContext.fragmentDeclarations.add(textureBinding);
strandsContext.fragmentDeclarations.add(samplerBinding);
bindingIndex += 2;
}
}
},
addStorageBufferBindingsToDeclarations(strandsContext) {
if (!strandsContext.renderer || !strandsContext.baseShader) return;
const isComputeShader = strandsContext.baseShader.shaderType === 'compute';
let bindingIndex = strandsContext.renderer.getNextBindingIndex({
vert: strandsContext.baseShader._vertSrc,
frag: strandsContext.baseShader._fragSrc,
compute: strandsContext.baseShader._computeSrc,
});
for (const {name, typeInfo} of strandsContext.uniforms) {
if (typeInfo.baseType === 'storage') {
const accessMode = isComputeShader ? 'read_write' : 'read';
let declaration;
if (typeInfo.schema) {
const structTypeName = `${name}Element`;
declaration = `struct ${structTypeName} ${typeInfo.schema.structBody}\n@group(0) @binding(${bindingIndex}) var<storage, ${accessMode}> ${name}: array<${structTypeName}>;`;
} else {
declaration = `@group(0) @binding(${bindingIndex}) var<storage, ${accessMode}> ${name}: array<f32>;`;
}
if (isComputeShader) {
strandsContext.computeDeclarations.add(declaration);
} else {
strandsContext.vertexDeclarations.add(declaration);
strandsContext.fragmentDeclarations.add(declaration);
}
bindingIndex += 1;
}
}
},
getTypeName(baseType, dimension) {
const primitiveTypeName = TypeNames[baseType + dimension];
if (!primitiveTypeName) {
return baseType;
}
return primitiveTypeName;
},
getNoiseShaderSnippet() {
return noiseWGSL;
},
getRandomFragmentShaderSnippet() {
return randomWGSL;
},
getRandomVertexShaderSnippet() {
return randomVertWGSL;
},
getRandomComputeShaderSnippet() {
return randomComputeWGSL;
},
generateHookUniformKey(name, typeInfo) {
// For sampler2D types, we don't add them to the uniform struct,
// but we still need them in the shader's hooks object so that
// they can be set by users.
if (typeInfo.baseType === 'sampler2D') {
return `${name}: sampler2D`; // Signal that this should not be added to uniform struct
}
// For storage buffers, we don't add them to the uniform struct
// Instead, they become separate storage buffer bindings
if (typeInfo.baseType === 'storage') {
return null; // Signal that this should not be added to uniform struct
}
return `${name}: ${this.getTypeName(typeInfo.baseType, typeInfo.dimension)}`;
},
generateVaryingVariable(varName, typeInfo) {
const typeName = this.getTypeName(typeInfo.baseType, typeInfo.dimension);
return `${varName}: ${typeName}`;
},
generateLocalDeclaration(varName, typeInfo) {
const typeName = this.getTypeName(typeInfo.baseType, typeInfo.dimension);
return `var<private> ${varName}: ${typeName};`;
},
generateStatement(generationContext, dag, nodeID) {
const node = getNodeDataFromID(dag, nodeID);
// Generate the expression followed by semicolon (unless suppressed)
const semicolon = generationContext.suppressSemicolon ? '' : ';';
if (node.statementType === StatementType.DISCARD) {
generationContext.write(`discard${semicolon}`);
} else if (node.statementType === StatementType.BREAK) {
generationContext.write(`break${semicolon}`);
} else if (node.statementType === StatementType.EXPRESSION) {
const exprNodeID = node.dependsOn[0];
const expr = this.generateExpression(generationContext, dag, exprNodeID);
generationContext.write(`${expr}${semicolon}`);
} else if (node.statementType === StatementType.EMPTY) {
// Generate just a semicolon (unless suppressed)
generationContext.write(semicolon);
} else if (node.statementType === StatementType.EARLY_RETURN) {
if (node.dependsOn && node.dependsOn.length > 0) {
const exprNodeID = node.dependsOn[0];
const expr = this.generateExpression(generationContext, dag, exprNodeID);
generationContext.write(`return ${expr}${semicolon}`);
} else {
generationContext.write(`return${semicolon}`);
}
}
},
generateAssignment(generationContext, dag, nodeID) {
const node = getNodeDataFromID(dag, nodeID);
// dependsOn[0] = targetNodeID, dependsOn[1] = sourceNodeID
const targetNodeID = node.dependsOn[0];
const sourceNodeID = node.dependsOn[1];
const targetNode = getNodeDataFromID(dag, targetNodeID);
const semicolon = generationContext.suppressSemicolon ? '' : ';';
// Check if target is an array access (storage buffer assignment)
if (targetNode.opCode === OpCode.Binary.ARRAY_ACCESS) {
const [bufferID, indexID] = targetNode.dependsOn;
const bufferExpr = this.generateExpression(generationContext, dag, bufferID);
const indexExpr = this.generateExpression(generationContext, dag, indexID);
const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID);
const fieldSuffix = targetNode.identifier ? `.${targetNode.identifier}` : '';
generationContext.write(`${bufferExpr}[i32(${indexExpr})]${fieldSuffix} = ${sourceExpr}${semicolon}`);
return;
}
// Check if target is a swizzle assignment
if (targetNode.opCode === OpCode.Unary.SWIZZLE) {
const parentID = targetNode.dependsOn[0];
const parentNode = getNodeDataFromID(dag, parentID);
const parentExpr = this.generateExpression(generationContext, dag, parentID);
const swizzle = targetNode.swizzle;
const parentDimension = parentNode.dimension;
const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID);
// Create an array for each element of the target variable
const componentMap = [];
for (let i = 0; i < parentDimension; i++) {
componentMap[i] = { target: 'self', index: i };
}
// Map swizzle characters to component indices
const getComponentIndex = (char) => {
if ('xyzw'.includes(char)) return 'xyzw'.indexOf(char);
if ('rgba'.includes(char)) return 'rgba'.indexOf(char);
return -1;
};
// Update the component map based on the swizzle assignment
for (let i = 0; i < swizzle.length; i++) {
const targetComponentIndex = getComponentIndex(swizzle[i]);
if (targetComponentIndex >= 0 && targetComponentIndex < parentDimension) {
componentMap[targetComponentIndex] = { target: 'rhs', index: i };
}
}
// Generate the reconstruction expression
const vectorTypeName = this.getTypeName(parentNode.baseType, parentDimension);
const components = componentMap.map(({ target, index }) => {
return `${target === 'self' ? parentExpr : sourceExpr}.${'xyzw'[index]}`
});
generationContext.write(`${parentExpr} = ${vectorTypeName}(${components.join(', ')})${semicolon}`);
} else {
// Regular assignment
const targetExpr = this.generateExpression(generationContext, dag, targetNodeID);
const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID);
// Generate assignment if we have both target and source
if (targetExpr && sourceExpr && targetExpr !== sourceExpr) {
generationContext.write(`${targetExpr} = ${sourceExpr}${semicolon}`);
}
}
},
generateDeclaration(generationContext, dag, nodeID) {
const expr = this.generateExpression(generationContext, dag, nodeID);
const tmp = `T${generationContext.nextTempID++}`;
generationContext.tempNames[nodeID] = tmp;
const T = extractNodeTypeInfo(dag, nodeID);
const typeName = this.getTypeName(T.baseType, T.dimension);
return `var ${tmp}: ${typeName} = ${expr};`;
},
generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) {
if (!returnType) {
generationContext.write('return;');
return;
}
const dag = strandsContext.dag;
const rootNode = getNodeDataFromID(dag, rootNodeID);
if (isStructType(returnType) && rootNode.identifier) {
const structTypeInfo = returnType;
for (let i = 0; i < structTypeInfo.properties.length; i++) {
const prop = structTypeInfo.properties[i];
const val = this.generateExpression(generationContext, dag, rootNode.dependsOn[i]);
if (prop.name !== val) {
generationContext.write(
`${rootNode.identifier}.${prop.name} = ${val};`
);
}
}
}
generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`);
},
generateExpression(generationContext, dag, nodeID) {
const node = getNodeDataFromID(dag, nodeID);
if (generationContext.tempNames?.[nodeID]) {
return generationContext.tempNames[nodeID];
}
switch (node.nodeType) {
case NodeType.LITERAL:
if (node.baseType === BaseType.FLOAT) {
return node.value.toFixed(4);
}
else {
return node.value;
}
case NodeType.VARIABLE:
// Track shared variable usage context
if (generationContext.shaderContext && generationContext.strandsContext?.sharedVariables?.has(node.identifier)) {
const sharedVar = generationContext.strandsContext.sharedVariables.get(node.identifier);
if (generationContext.shaderContext === 'vertex') {
sharedVar.usedInVertex = true;
} else if (generationContext.shaderContext === 'fragment') {
sharedVar.usedInFragment = true;
}
}
// Detect instanceID usage in fragment context and rewrite to varying name
if (node.identifier === this.instanceIdReference() && generationContext.shaderContext === 'fragment') {
generationContext.strandsContext._instanceIDUsedInFragment = true;
return INSTANCE_ID_VARYING_NAME;
}
// Check if this is a uniform variable (but not a texture or storage buffer)
const uniform = generationContext.strandsContext?.uniforms?.find(uniform => uniform.name === node.identifier);
if (uniform && uniform.typeInfo.baseType !== 'sampler2D' && uniform.typeInfo.baseType !== 'storage') {
return `hooks.${node.identifier}`;
}
return node.identifier;
case NodeType.OPERATION:
const useParantheses = node.usedBy.length > 0;
if (node.opCode === OpCode.Nary.CONSTRUCTOR) {
// TODO: differentiate casts and constructors for more efficient codegen.
// if (node.dependsOn.length === 1 && node.dimension === 1) {
// return this.generateExpression(generationContext, dag, node.dependsOn[0]);
// }
if (node.baseType === BaseType.SAMPLER2D) {
return this.generateExpression(generationContext, dag, node.dependsOn[0]);
}
const T = this.getTypeName(node.baseType, node.dimension);
const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep));
return `${T}(${deps.join(', ')})`;
}
if (node.opCode === OpCode.Nary.TERNARY) {
const [condID, trueID, falseID] = node.dependsOn;
const cond = this.generateExpression(generationContext, dag, condID);
const trueExpr = this.generateExpression(generationContext, dag, trueID);
const falseExpr = this.generateExpression(generationContext, dag, falseID);
return `select(${falseExpr}, ${trueExpr}, ${cond})`;
}
if (node.opCode === OpCode.Nary.FUNCTION_CALL) {
// Convert mod() function calls to % operator in WGSL
if (node.identifier === 'mod' && node.dependsOn.length === 2) {
const [leftID, rightID] = node.dependsOn;
const left = this.generateExpression(generationContext, dag, leftID);
const right = this.generateExpression(generationContext, dag, rightID);
const useParantheses = node.usedBy.length > 0;
if (useParantheses) {
return `(${left} % ${right})`;
} else {
return `${left} % ${right}`;
}
}
// Convert atan(y, x) to atan2(y, x) in WGSL
if (node.identifier === 'atan' && node.dependsOn.length === 2) {
const functionArgs = node.dependsOn.map(arg => this.generateExpression(generationContext, dag, arg));
return `atan2(${functionArgs.join(', ')})`;
}
const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg));
if (node.identifier === 'random') {
const ctx = generationContext.shaderContext;
if (ctx === 'fragment') {
functionArgs.push('_p5FragPos.xy');
} else if (ctx === 'vertex') {
functionArgs.push('f32(_p5VertexId)');
} else if (ctx === 'compute') {
functionArgs.push('_p5GlobalId');
}
}
return `${node.identifier}(${functionArgs.join(', ')})`;
}
if (node.opCode === OpCode.Binary.MEMBER_ACCESS) {
const [lID, rID] = node.dependsOn;
const lName = this.generateExpression(generationContext, dag, lID);
const rName = this.generateExpression(generationContext, dag, rID);
return `${lName}.${rName}`;
}
if (node.opCode === OpCode.Unary.SWIZZLE) {
const parentID = node.dependsOn[0];
const parentExpr = this.generateExpression(generationContext, dag, parentID);
return `${parentExpr}.${node.swizzle}`;
}
if (node.opCode === OpCode.Binary.ARRAY_ACCESS) {
const [bufferID, indexID] = node.dependsOn;
const bufferExpr = this.generateExpression(generationContext, dag, bufferID);
const indexExpr = this.generateExpression(generationContext, dag, indexID);
const fieldSuffix = node.identifier ? `.${node.identifier}` : '';
return `${bufferExpr}[i32(${indexExpr})]${fieldSuffix}`;
}
if (node.dependsOn.length === 2) {
const [lID, rID] = node.dependsOn;
const left = this.generateExpression(generationContext, dag, lID);
const right = this.generateExpression(generationContext, dag, rID);
// In WGSL, % operator works for both floats and integers
if (node.opCode === OpCode.Binary.MODULO) {
return `(${left} % ${right})`;
}
const opSym = OpCodeToSymbol[node.opCode];
if (useParantheses) {
return `(${left} ${opSym} ${right})`;
} else {
return `${left} ${opSym} ${right}`;
}
}
if (node.opCode === OpCode.Unary.LOGICAL_NOT
|| node.opCode === OpCode.Unary.NEGATE
|| node.opCode === OpCode.Unary.PLUS
) {
const [i] = node.dependsOn;
const val = this.generateExpression(generationContext, dag, i);
const sym = OpCodeToSymbol[node.opCode];
return `${sym}${val}`;
}
case NodeType.PHI:
// Phi nodes represent conditional merging of values
// If this phi node has an identifier (like varying variables), use that
if (node.identifier) {
return node.identifier;
}
// Otherwise, they should have been declared as temporary variables
// and assigned in the appropriate branches
if (generationContext.tempNames?.[nodeID]) {
return generationContext.tempNames[nodeID];
} else {
// If no temp was created, this phi node only has one input
// so we can just use that directly
const validInputs = node.dependsOn.filter(id => id !== null);
if (validInputs.length > 0) {
return this.generateExpression(generationContext, dag, validInputs[0]);
} else {
throw new Error('No valid inputs for node');
}
}
case NodeType.ASSIGNMENT:
internalError(`ASSIGNMENT nodes should not be used as expressions`);
default:
internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`);
}
},
generateBlock(blockID, strandsContext, generationContext) {
const type = strandsContext.cfg.blockTypes[blockID];
const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT];
handler.call(cfgHandlers, blockID, strandsContext, generationContext);
},
createGetTextureCall(strandsContext, args) {
// In WebGPU, we need to add a sampler argument for the texture call
// First argument should be a texture, second should be coordinates
// We need to augment with a sampler argument based on the texture name
const textureArg = args[0];
const coordsArg = args[1];
// Create a sampler variable node - add "_sampler" suffix to the texture identifier
const { dag } = strandsContext;
const textureNode = getNodeDataFromID(dag, textureArg.id);
const samplerIdentifier = textureNode.identifier + '_sampler';
const samplerVariable = variableNode(strandsContext, { baseType: BaseType.SAMPLER, dimension: 1 }, samplerIdentifier);
const samplerNode = createStrandsNode(samplerVariable.id, samplerVariable.dimension, strandsContext);
// Create a LOD literal node (0.0) so we can use textureSampleLevel instead
// of textureSample. textureSample doesn't let you use uniform values in control
// flow, whereas textureSampleLevel does. While we don't have mipmaps, we don't
// miss out.
// TODO: if we *do* add mipmap support, update this logic -- we'd need to hoist
// the texture lookup out of the control flow.
const lodLiteral = scalarLiteralNode(
strandsContext,
{ dimension: 1, baseType: BaseType.FLOAT },
0.0
);
const lodNode = createStrandsNode(lodLiteral.id, lodLiteral.dimension, strandsContext);
// Create the augmented args: [texture, sampler, coords, lod]
const augmentedArgs = [textureArg, samplerNode, coordsArg, lodNode];
const { id, dimension } = functionCallNode(strandsContext, 'textureSampleLevel', augmentedArgs, {
overloads: [{
params: [DataType.sampler2D, DataType.sampler, DataType.float2, DataType.float1],
returnType: DataType.float4
}]
});
return { id, dimension };
},
instanceIdReference() {
return 'instanceID';
},
generateInstanceIDVarying() {
return { name: INSTANCE_ID_VARYING_NAME, declaration: `${INSTANCE_ID_VARYING_NAME}: i32`, source: 'i32(instanceID)', interpolation: 'flat' };
},
};
const filterUniforms = `
// Group 0: Filter Properties
struct FilterUniforms {
canvasSize: vec2<f32>,
texelSize: vec2<f32>,
}
// Group 1: Model Transform
struct ModelUniforms {
uModelViewMatrix: mat4x4<f32>,
}
// Group 2: Camera and Projection
struct CameraUniforms {
uProjectionMatrix: mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> filterParams: FilterUniforms;
@group(0) @binding(1) var tex0: texture_2d<f32>;
@group(0) @binding(2) var tex0_sampler: sampler;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
`;
const baseFilterVertexShader = filterUniforms + `
struct VertexInput {
@location(0) aPosition: vec3<f32>,
@location(1) aTexCoord: vec2<f32>,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) vTexCoord: vec2<f32>,
}
@vertex
fn main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
// transferring texcoords for the frag shader
output.vTexCoord = input.aTexCoord;
// copy position with a fourth coordinate for projection (1.0 is normal)
let positionVec4 = vec4<f32>(input.aPosition, 1.0);
// project to 3D space
output.position = camera.uProjectionMatrix * model.uModelViewMatrix * positionVec4;
return output;
}
`;
const baseFilterFragmentShader = filterUniforms + `
struct FilterInputs {
texCoord: vec2<f32>,
canvasSize: vec2<f32>,
texelSize: vec2<f32>,
}
struct FragmentInput {
@location(0) vTexCoord: vec2<f32>,
}
struct FragmentOutput {
@location(0) color: vec4<f32>,
}
@fragment
fn main(input: FragmentInput) -> FragmentOutput {
var output: FragmentOutput;
var inputs: FilterInputs;
inputs.texCoord = input.vTexCoord;
inputs.canvasSize = filterParams.canvasSize;
inputs.texelSize = filterParams.texelSize;
var outColor = HOOK_getColor(inputs, tex0, tex0_sampler);
outColor = vec4<f32>(outColor.rgb * outColor.a, outColor.a);
output.color = outColor;
return output;
}
`;
const uniforms = `
struct Uniforms {
uModelViewMatrix: mat4x4<f32>,
uProjectionMatrix: mat4x4<f32>,
uNormalMatrix: mat3x3<f32>,
roughness: f32,
};
`;
// Shared WGSL functions
const sharedFunctions = `
const PI = 3.14159265359;
fn nTOE(v: vec3<f32>) -> vec2<f32> {
// x = r sin(phi) cos(theta)
// y = r cos(phi)
// z = r sin(phi) sin(theta)
let phi = acos(v.y);
// if phi is 0, then there are no x, z components
var theta = 0.0;
// else
theta = acos(v.x / sin(phi));
let sinTheta = v.z / sin(phi);
if (sinTheta < 0.0) {
// Turn it into -theta, but in the 0-2PI range
theta = 2.0 * PI - theta;
}
theta = theta / (2.0 * PI);
let phiNorm = phi / PI;
return vec2<f32>(phiNorm, theta);
}
fn random(p: vec2<f32>) -> f32 {
let p3 = fract(vec3<f32>(p.x, p.y, p.x) * 0.1031);
let dotP3 = dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
`;
const imageLightVertexShader = `
struct VertexInput {
@location(0) aPosition: vec3<f32>,
@location(1) aNormal: vec3<f32>,
@location(2) aTexCoord: vec2<f32>,
}
struct VertexOutput {
@builtin(position) Position: vec4<f32>,
@location(0) localPos: vec3<f32>,
@location(1) vWorldNormal: vec3<f32>,
@location(2) vWorldPosition: vec3<f32>,
@location(3) vTexCoord: vec2<f32>,
}
${uniforms}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@vertex
fn main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
// Multiply the position by the matrix
let viewModelPosition = uniforms.uModelViewMatrix * vec4<f32>(input.aPosition, 1.0);
output.Position = uniforms.uProjectionMatrix * viewModelPosition;
// Orient the normals and pass to the fragment shader
output.vWorldNormal = uniforms.uNormalMatrix * input.aNormal;
// Send the view position to the fragment shader
output.vWorldPosition = viewModelPosition.xyz;
output.localPos = output.vWorldPosition;
output.vTexCoord = input.aTexCoord;
return output;
}
`;
const imageLightDiffusedFragmentShader = `
struct FragmentInput {
@location(0) localPos: vec3<f32>,
@location(3) vTexCoord: vec2<f32>,
}
${uniforms}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var environmentMap: texture_2d<f32>;
@group(0) @binding(2) var environmentMap_sampler: sampler;
${sharedFunctions}
@fragment
fn main(input: FragmentInput) -> @location(0) vec4<f32> {
// The sample direction equals the hemisphere's orientation
let phi = input.vTexCoord.x * 2.0 * PI;
let theta = input.vTexCoord.y * PI;
let x = sin(theta) * cos(phi);
let y = sin(theta) * sin(phi);
let z = cos(theta);
let normal = vec3<f32>(x, y, z);
// Discretely sampling the hemisphere given the integral's
// spherical coordinates translates to the following fragment code:
var irradiance = vec3<f32>(0.0);
let up = vec3<f32>(0.0, 1.0, 0.0);
let right = normalize(cross(up, normal));
let upNorm = normalize(cross(normal, right));
// We specify a fixed sampleDelta delta value to traverse
// the hemisphere; decreasing or increasing the sample delta
// will increase or decrease the accuracy respectively.
let sampleDelta = 0.100;
var nrSamples = 0.0;
let randomOffset = random(input.vTexCoord) * sampleDelta;
for (var rawPhi = 0.0; rawPhi < 2.0 * PI; rawPhi += sampleDelta) {
let phiSample = rawPhi + randomOffset;
for (var rawTheta = 0.0; rawTheta < 0.5 * PI; rawTheta += sampleDelta) {
let thetaSample = rawTheta + randomOffset;
// spherical to cartesian (in tangent space) // tangent space to world // add each sample result to irradiance
let xSample = sin(thetaSample) * cos(phiSample);
let ySample = sin(thetaSample) * sin(phiSample);
let zSample = cos(thetaSample);
let tangentSample = vec3<f32>(xSample, ySample, zSample);
let sampleVec = tangentSample.x * right + tangentSample.y * upNorm + tangentSample.z * normal;
let envSample = textureSample(environmentMap, environmentMap_sampler, nTOE(sampleVec));
irradiance += envSample.xyz * cos(thetaSample) * sin(thetaSample);
nrSamples += 1.0;
}
}
// divide by the total number of samples taken, giving us the average sampled irradiance.
irradiance = PI * irradiance * (1.0 / nrSamples);
return vec4<f32>(irradiance, 1.0);
}
`;
const imageLightSpecularFragmentShader = `
struct FragmentInput {
@location(0) localPos: vec3<f32>,
@location(3) vTexCoord: vec2<f32>,
}
${uniforms}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var environmentMap: texture_2d<f32>;
@group(0) @binding(2) var environmentMap_sampler: sampler;
${sharedFunctions}
fn VanDerCorput(nIn: i32, base: i32) -> f32 {
// Use the bit manipulation version for WebGPU (equivalent to WEBGL2 version)
var n = u32(nIn);
n = (n << 16u) | (n >> 16u);
n = ((n & 0x55555555u) << 1u) | ((n & 0xAAAAAAAAu) >> 1u);
n = ((n & 0x33333333u) << 2u) | ((n & 0xCCCCCCCCu) >> 2u);
n = ((n & 0x0F0F0F0Fu) << 4u) | ((n & 0xF0F0F0F0u) >> 4u);
n = ((n & 0x00FF00FFu) << 8u) | ((n & 0xFF00FF00u) >> 8u);
return f32(n) * 2.3283064365386963e-10; // / 0x100000000
}
fn HammersleyNoBitOps(i: i32, N: i32) -> vec2<f32> {
return vec2<f32>(f32(i) / f32(N), VanDerCorput(i, 2));
}
fn ImportanceSampleGGX(Xi: vec2<f32>, N: vec3<f32>, roughness: f32) -> vec3<f32> {
let a = roughness * roughness;
let phi = 2.0 * PI * Xi.x;
let cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y));
let sinTheta = sqrt(1.0 - cosTheta * cosTheta);
// from spherical coordinates to cartesian coordinates
var H: vec3<f32>;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
// from tangent-space vector to world-space sample vector
let up = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), abs(N.z) < 0.999);
let tangent = normalize(cross(up, N));
let bitangent = cross(N, tangent);
let sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
return normalize(sampleVec);
}
@fragment
fn main(input: FragmentInput) -> @location(0) vec4<f32> {
let SAMPLE_COUNT = 400i; // 4096
let lowRoughnessLimit = i32(pow(2.0, (uniforms.roughness + 0.1) * 20.0));
var totalWeight = 0.0;
var prefilteredColor = vec3<f32>(0.0);
let phi = input.vTexCoord.x * 2.0 * PI;
let theta = input.vTexCoord.y * PI;
let x = sin(theta) * cos(phi);
let y = sin(theta) * sin(phi);
let z = cos(theta);
let N = vec3<f32>(x, y, z);
let V = N;
for (var i = 0i; i < SAMPLE_COUNT; i++) {
// break at smaller sample numbers for low roughness levels
if (i == lowRoughnessLimit) {
break;
}
let Xi = HammersleyNoBitOps(i, SAMPLE_COUNT);
let H = ImportanceSampleGGX(Xi, N, uniforms.roughness);
let L = normalize(2.0 * dot(V, H) * H - V);
let NdotL = max(dot(N, L), 0.0);
// Always sample the texture to maintain uniform control flow
let envSample = textureSample(environmentMap, environmentMap_sampler, nTOE(L));
// Only add to accumulators if NdotL > 0
if (NdotL > 0.0) {
prefilteredColor += envSample.xyz * NdotL;
totalWeight += NdotL;
}
}
prefilteredColor = prefilteredColor / totalWeight;
return vec4<f32>(prefilteredColor, 1.0);
}
`;
const baseComputeShader = `
struct ComputeUniforms {
uTotalCount: vec3<i32>,
uPhysicalCount: vec3<i32>,
}
@group(0) @binding(0) var<uniform> uniforms: ComputeUniforms;
@compute @workgroup_size(8, 8, 1)
fn main(
@builtin(global_invocation_id) globalId: vec3<u32>,
@builtin(local_invocation_id) localId: vec3<u32>,
@builtin(workgroup_id) workgroupId: vec3<u32>,
@builtin(local_invocation_index) localIndex: u32
) {
let totalIterations = u32(uniforms.uTotalCount.x) * u32(uniforms.uTotalCount.y) * u32(uniforms.uTotalCount.z);
let physicalId = globalId.x + globalId.y * (u32(uniforms.uPhysicalCount.x)) + globalId.z * (u32(uniforms.uPhysicalCount.x) * u32(uniforms.uPhysicalCount.y));
if (physicalId >= totalIterations) {
return;
}
var index = vec3<i32>(0);
index.x = i32(physicalId % u32(uniforms.uTotalCount.x));
let remainingY = physicalId / u32(uniforms.uTotalCount.x);
index.y = i32(remainingY % u32(uniforms.uTotalCount.y));
index.z = i32(remainingY / u32(uniforms.uTotalCount.y));
HOOK_iteration(index);
}
`;
/**
* @module 3D
* @submodule p5.strands
* @for p5
*/
const FRAME_STATE = {
PENDING: 0,
UNPROMOTED: 1,
PROMOTED: 2
};
function rendererWebGPU(p5, fn) {
const { lineDefs } = getStrokeDefs((n, v, t) => `const ${n}: ${t} = ${v};\n`);
// RendererWebGPU depends on these other classes being set up prior,
// as it is optimized for being in a standalone build, not core
const {
Renderer3D,
Shader,
Texture,
MipmapTexture,
Image,
Camera,
RGBA,
} = p5;
class StorageBuffer {
constructor(buffer, size, renderer, schema = null) {
this._isStorageBuffer = true;
this.buffer = buffer;
this.size = size;
this._renderer = renderer;
this._schema = schema;
}
/**
* Updates the data in the buffer with new values. The new data must be in
* the same format as the data originally passed to
* <a href="#/p5/createStorage">`createStorage()`</a>.
*
* ```js example
* let particles;
* let computeShader;
* let displayShader;
* let instance;
* const numParticles = 100;
*
* async function setup() {
* await createCanvas(100, 100, WEBGPU);
* particles = createStorage(makeParticles(width / 2, height / 2));
* computeShader = buildComputeShader(simulate);
* displayShader = buildMaterialShader(display);
* instance = buildGeometry(drawParticle);
* describe('100 orange particles shooting outward.');
* }
*
* function makeParticles(x, y) {
* let data = [];
* for (let i = 0; i < numParticles; i++) {
* let angle = (i / numParticles) * TWO_PI;
* let speed = random(0.5, 2);
* data.push({
* position: createVector(x, y),
* velocity: createVector(cos(angle) * speed, sin(angle) * speed),
* });
* }
* return data;
* }
*
* function drawParticle() {
* sphere(2);
* }
*
* function simulate() {
* let data = uniformStorage(particles);
* let idx = index.x;
* data[idx].position = data[idx].position + data[idx].velocity;
* }
*
* function display() {
* let data = uniformStorage(particles);
* worldInputs.begin();
* let pos = data[instanceID()].position;
* worldInputs.position.xy += pos - [width / 2, height / 2];
* worldInputs.end();
* }
*
* function draw() {
* background(30);
* if (frameCount % 60 === 0) {
* particles.update(makeParticles(random(width), random(height)));
* }
* compute(computeShader, numParticles);
* noStroke();
* fill(255, 200, 50);
* shader(displayShader);
* model(instance, numParticles);
* }
* ```
*
* @method update
* @for p5.StorageBuffer
* @beta
* @webgpu
* @webgpuOnly
* @param {Number[]|Float32Array|Object[]} data The new data to write into the buffer.
*/
update(data) {
const device = this._renderer.device;
if (this._schema !== null) {
// Buffer was created with a struct array
if (
!Array.isArray(data) ||
data.length === 0 ||
typeof data[0] !== 'object' ||
Array.isArray(data[0])
) {
throw new Error(
'update() expects an array of objects matching the original struct format'
);
}
const newSchema = this._renderer._inferStructSchema(data[0]);
if (newSchema.structBody !== this._schema.structBody) {
throw new Error(
`update() data structure doesn't match the original.\n` +
` Expected: ${this._schema.structBody}\n` +
` Got: ${newSchema.structBody}`
);
}
const packed = this._renderer._packStructArray(data, this._schema);
if (packed.byteLength > this.size) {
throw new Error(
`update() data (${packed.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
);
}
device.queue.writeBuffer(this.buffer, 0, packed);
} else {
// Buffer was created with a float array
let floatData;
if (data instanceof Float32Array) {
floatData = data;
} else if (Array.isArray(data)) {
floatData = new Float32Array(data);
} else {
throw new Error(
'update() expects a Float32Array or array of numbers for this buffer'
);
}
if (floatData.byteLength > this.size) {
throw new Error(
`update() data (${floatData.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
);
}
device.queue.writeBuffer(this.buffer, 0, floatData);
}
}
/**
* Reads data from a storage buffer back into JavaScript.
*
* Copies data from the GPU to the CPU using a temporary buffer,
* so it must be awaited. Returns a `Float32Array` for number
* buffers, or an array of plain objects for struct buffers.
*
* Note: This is a GPU -> CPU read, so calling it often (like every frame)
* can be slow.
*
* ```js example
* let data;
* let computeShader;
*
* async function setup() {
* await createCanvas(100, 100, WEBGPU);
*
* data = createStorage(new Float32Array([1, 2, 3, 4]));
* computeShader = buildComputeShader(doubleValues);
* compute(computeShader, 4);
*
* let result = await data.read();
* // result is Float32Array [2, 4, 6, 8]
* for (let i = 0; i < result.length; i++) {
* print(result[i]);
* }
* describe('Prints the values 2, 4, 6, 8 to the console.');
* }
*
* function doubleValues() {
* let d = uniformStorage(data);
* let idx = index.x;
* d[idx] = d[idx] * 2;
* }
* ```
*
* @method read
* @for p5.StorageBuffer
* @beta
* @webgpu
* @webgpuOnly
* @returns {Promise<Float32Array|Object[]>}
*/
async read() {
const device = this._renderer.device;
this._renderer.flushDraw();
const stagingBuffer = device.createBuffer({
size: this.size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const commandEncoder = device.createCommandEncoder();
commandEncoder.copyBufferToBuffer(this.buffer, 0, stagingBuffer, 0, this.size);
device.queue.submit([commandEncoder.finish()]);
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, this.size);
const mappedRange = stagingBuffer.getMappedRange(0, this.size);
// Copy before unmapping because mapped memory becomes invalid after unmap
const rawCopy = new Float32Array(mappedRange.byteLength / 4);
rawCopy.set(new Float32Array(mappedRange));
stagingBuffer.unmap();
stagingBuffer.destroy();
if (this._schema !== null) {
return this._renderer._unpackStructArray(rawCopy, this._schema);
}
return rawCopy;
}
/**
* Updates a single element in the buffer at a given index. Use this
* when only a small number of elements need to change. If you need to
* replace all the data at once, use
* <a href="#/p5.StorageBuffer/update">`update()`</a> instead.
*
* ```js
* let buf;
*
* async function setup() {
* await createCanvas(100, 100, WEBGPU);
*
* // Float buffer: update one value by index
* buf = createStorage(new Float32Array([1, 2, 3, 4]));
* buf.set(2, 9.5); // only index 2 changes → [1, 2, 9.5, 4]
*
* let result = await buf.read();
* print(result[2]); // 9.5
* describe('Prints 9.5 to the console.');
* }
* ```
*
* ```js
* let particles;
* const numParticles = 100;
*
* async function setup() {
* await createCanvas(100, 100, WEBGPU);
* particles = createStorage(makeParticles());
*
* // Replace particle 42 without touching the others
* particles.set(42, {
* position: createVector(0, 0),
* velocity: createVector(1, 0),
* });
*
* // Read back to confirm the update
* let result = await particles.read();
* print(result[42].position.x, result[42].position.y); // 0, 0
* describe('Prints the position of particle 42 after updating it.');
* }
*
* function makeParticles() {
* let data = [];
* for (let i = 0; i < numParticles; i++) {
* data.push({
* position: createVector(random(width), random(height)),
* velocity: createVector(random(-1, 1), random(-1, 1)),
* });
* }
* return data;
* }
* ```
*
* @method set
* @for p5.StorageBuffer
* @beta
* @webgpu
* @webgpuOnly
* @param {Number} index The zero-based index of the element to update.
* @param {Number|Object} value The new value. Pass a number for float
* buffers, or a plain object matching the original struct layout for
* struct buffers.
*/
set(index, value) {
const device = this._renderer.device;
if (this._schema !== null) {
// buffer was created with an array of structs
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(
'set() expects a plain object matching the original struct format for this buffer'
);
}
const { stride } = this._schema;
const byteOffset = index * stride;
if (byteOffset + stride > this.size) {
throw new Error(
`set() index ${index} is out of bounds for this buffer ` +
`(buffer holds ${Math.floor(this.size / stride)} elements)`
);
}
// pack just this one element using the same logic as update()
const packed = this._renderer._packStructArray([value], this._schema);
// use packed.buffer (ArrayBuffer) so the size arg is always in bytes
device.queue.writeBuffer(this.buffer, byteOffset, packed.buffer, 0, stride);
} else {
// buffer was created with a float array
if (typeof value !== 'number') {
throw new Error(
'set() expects a number for this float buffer'
);
}
const byteOffset = index * 4;
if (byteOffset + 4 > this.size) {
throw new Error(
`set() index ${index} is out of bounds for this buffer ` +
`(buffer holds ${Math.floor(this.size / 4)} floats)`
);
}
device.queue.writeBuffer(this.buffer, byteOffset, new Float32Array([value]));
}
}
}
/**
* A block of data that shaders can read from, and compute shaders can also
* write to. This is only available in WebGPU mode.
*
* Note: <a href="#/p5/createStorage">`createStorage()`</a> is the recommended
* way to create an instance of this class.
*
* @class p5.StorageBuffer
* @beta
* @webgpu
* @webgpuOnly
*/
p5.StorageBuffer = StorageBuffer;
class RendererWebGPU extends Renderer3D {
constructor(pInst, w, h, isMainCanvas, elt) {
super(pInst, w, h, isMainCanvas, elt);
// Used to group draws into one big render pass
this.activeRenderPass = null;
this.activeRenderPassEncoder = null;
this.activeShaderOptions = null;
this.activeShader = null;
this.samplers = new Map();
// Some uniforms update every frame, like model matrices and sometimes colors.
// The fastest way to handle these is to use mapped memory. We'll batch those
// into bigger buffers with dynamic offsets, separate from the usual system
// where bind groups have their own little buffers that get cached when they
// are unchanged
this.uniformBufferAlignment = 256;
this.activeUniformBuffers = [];
this.currentUniformBuffer = undefined;
this.uniformBufferPool = [];
this.resettingUniformBuffers = [];
this.dynamicEntryOffsets = new Uint32Array(64);
// Cache for current frame's canvas texture view
this.currentCanvasColorTexture = null;
this.currentCanvasColorTextureView = null;
// Single reusable staging buffer for pixel reading
this.pixelReadBuffer = null;
this.pixelReadBufferSize = 0;
this.strandsBackend = wgslBackend;
// Registry to track all shaders for uniform data pooling
this._shadersWithPools = [];
// Registry to track geometries with buffer pools
this._geometriesWithPools = [];
// Flag to track if any draws have happened that need queue submission
this._hasPendingDraws = false;
this._pendingCommandEncoders = [];
// Queue of callbacks to run after next submit (mainly for safe texture deletion)
this._postSubmitCallbacks = [];
// Retired buffers to destroy at end of frame
this._retiredBuffers = [];
// Storage buffers for compute shaders
this._storageBuffers = new Set();
// 2D canvas for pixel reading fallback
this._pixelReadCanvas = null;
this._pixelReadCtx = null;
this.mainFramebuffer = null;
this._frameState = FRAME_STATE.PENDING;
this.finalCamera = new Camera(this);
this.finalCamera._computeCameraDefaultSettings();
this.finalCamera._setDefaultCamera();
this.depthFormat = 'depth24plus-stencil8';
this.depthTexture = null;
this.depthTextureView = null;
}
async setupContext() {
this._setAttributeDefaults(this._pInst);
await this._initContext();
}
_setAttributeDefaults(pInst) {
const defaults = {
forceFallbackAdapter: false,
powerPreference: 'high-performance',
};
if (pInst._webgpuAttributes === null) {
pInst._webgpuAttributes = defaults;
} else {
pInst._webgpuAttributes = Object.assign(defaults, pInst._webgpuAttributes);
}
return;
}
async _initContext() {
this.adapter = await navigator.gpu?.requestAdapter(this._webgpuAttributes);
this.device = await this.adapter?.requestDevice({
// Todo: check support
requiredFeatures: ['depth32float-stencil8']
});
if (!this.device) {
throw new Error('Your browser does not support WebGPU.');
}
this.queue = this.device.queue;
this.drawingContext = this.canvas.getContext('webgpu');
this.presentationFormat = navigator.gpu.getPreferredCanvasFormat();
this.drawingContext.configure({
device: this.device,
format: this.presentationFormat,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
alphaMode: 'premultiplied',
});
// TODO disablable stencil
this.mainFramebuffer = this.createFramebuffer({ _useCanvasFormat: true });
this._updateSize();
this._update();
this.flushDraw();
}
async _setAttributes(key, value) {
if (typeof this._pInst._webgpuAttributes === "undefined") {
console.log(
"You are trying to use setAttributes on a p5.Graphics object " +
"that does not use a WebGPU renderer."
);
return;
}
let unchanged = true;
if (typeof value !== "undefined") {
//first time modifying the attributes
if (this._pInst._webgpuAttributes === null) {
this._pInst._webgpuAttributes = {};
}
if (this._pInst._webgpuAttributes[key] !== value) {
//changing value of previously altered attribute
this._pInst._webgpuAttributes[key] = value;
unchanged = false;
}
//setting all attributes with some change
} else if (key instanceof Object) {
if (this._pInst._webgpuAttributes !== key) {
this._pInst._webgpuAttributes = key;
unchanged = false;
}
}
//@todo_FES
if (!this.isP3D || unchanged) {
return;
}
if (!this._pInst._setupDone) {
if (this.geometryBufferCache.numCached() > 0) {
p5._friendlyError(
"Sorry, Could not set the attributes, you need to call setAttributes() " +
"before calling the other drawing methods in setup()"
);
return;
}
}
await this._resetContext(null, null, RendererWebGPU);
if (this.states.curCamera) {
this.states.curCamera._renderer = this._renderer;
}
}
_updateSize() {
if (!this.device || !this.depthFormat) return;
if (this.depthTexture && this.depthTexture.destroy) {
this.flushDraw();
const textureToDestroy = this.depthTexture;
this._postSubmitCallbacks.push(() => textureToDestroy.destroy());
this.depthTextureView = null;
}
this.depthTexture = this.device.createTexture({
size: {
width: Math.ceil(this.width * this._pixelDensity),
height: Math.ceil(this.height * this._pixelDensity),
depthOrArrayLayers: 1,
},
format: this.depthFormat,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
});
this.depthTextureView = this.depthTexture.createView();
// Clear the main canvas after resize
this.clear();
}
_getCanvasColorTextureView() {
const canvasTexture = this.drawingContext.getCurrentTexture();
// If texture changed (new frame), update cache
if (this.currentCanvasColorTexture !== canvasTexture) {
this.currentCanvasColorTexture = canvasTexture;
this.currentCanvasColorTextureView = canvasTexture.createView();
}
return this.currentCanvasColorTextureView;
}
_beginActiveRenderPass() {
if (this.activeRenderPass) return;
// Use framebuffer texture if active, otherwise use canvas texture
const activeFramebuffer = this.activeFramebuffer();
const colorAttachment = {
view: activeFramebuffer
? (activeFramebuffer.aaColorTexture
? activeFramebuffer.aaColorTextureView
: activeFramebuffer.colorTextureView)
: this._getCanvasColorTextureView(),
loadOp: "load",
storeOp: "store",
// If using multisampled texture, resolve to non-multisampled texture
resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture
? activeFramebuffer.colorTextureView
: undefined,
};
// Use framebuffer depth texture if active, otherwise use canvas depth texture
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
const renderPassDescriptor = {
colorAttachments: [colorAttachment],
depthStencilAttachment: depthTextureView
? {
view: depthTextureView,
depthLoadOp: "load",
depthStoreOp: "store",
depthClearValue: 1.0,
stencilLoadOp: "load",
stencilStoreOp: "store",
depthReadOnly: false,
stencilReadOnly: false,
}
: undefined,
};
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
this.activeRenderPassEncoder = commandEncoder;
this.activeRenderPass = passEncoder;
}
_finishActiveRenderPass() {
if (!this.activeRenderPass) return;
const commandEncoder = this.activeRenderPassEncoder;
const passEncoder = this.activeRenderPass;
passEncoder.end();
// Store the command encoder for later submission
this._pendingCommandEncoders.push(commandEncoder.finish());
this.activeRenderPassEncoder = null;
this.activeRenderPass = null;
this.activeShader = null;
this.activeShaderOptions = null;
}
clear(...args) {
if (!this.device || !this.drawingContext) return;
const _r = args[0] || 0;
const _g = args[1] || 0;
const _b = args[2] || 0;
const _a = args[3] || 0;
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED.
// However, if we are still in setup (frameCount == 0), we must promote
// so that mainFramebuffer gets the cleared content. This ensures that if
// draw() later promotes without a copy, it starts from the correct state
// rather than a stale mainFramebuffer.
// Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED
// (i.e. calling background() some frames but not others) will still
// lose intermediate UNPROMOTED frame content.
if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) {
if (this._pInst.frameCount > 0) {
this._frameState = FRAME_STATE.UNPROMOTED;
} else {
this._promoteToFramebufferWithoutCopy();
// clear() then targets mainFramebuffer via activeFramebuffer()
}
}
this._finishActiveRenderPass();
const commandEncoder = this.device.createCommandEncoder();
// Use framebuffer texture if active, otherwise use canvas texture
const activeFramebuffer = this.activeFramebuffer();
const colorAttachment = {
view: activeFramebuffer
? (activeFramebuffer.aaColorTexture
? activeFramebuffer.aaColorTextureView
: activeFramebuffer.colorTextureView)
: this._getCanvasColorTextureView(),
clearValue: { r: _r * _a, g: _g * _a, b: _b * _a, a: _a },
loadOp: 'clear',
storeOp: 'store',
// If using multisampled texture, resolve to non-multisampled texture
resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture
? activeFramebuffer.colorTextureView
: undefined,
};
// Use framebuffer depth texture if active, otherwise use canvas depth texture
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
const depthAttachment = depthTextureView
? {
view: depthTextureView,
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
stencilLoadOp: 'load',
stencilStoreOp: 'store',
}
: undefined;
const renderPassDescriptor = {
colorAttachments: [colorAttachment],
...(depthAttachment ? { depthStencilAttachment: depthAttachment } : {}),
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
/**
* Resets all depth information so that nothing previously drawn will
* occlude anything subsequently drawn.
*/
clearDepth(depth = 1) {
if (!this.device || !this.depthTextureView) return;
this._finishActiveRenderPass();
const commandEncoder = this.device.createCommandEncoder();
// Use framebuffer texture if active, otherwise use canvas texture
const activeFramebuffer = this.activeFramebuffer();
// Use framebuffer depth texture if active, otherwise use canvas depth texture
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
if (!depthTextureView) {
// No depth buffer to clear
return;
}
const depthAttachment = {
view: depthTextureView,
depthClearValue: depth,
depthLoadOp: 'clear',
depthStoreOp: 'store',
stencilLoadOp: 'load',
stencilStoreOp: 'store',
};
const renderPassDescriptor = {
colorAttachments: [], // No color attachments, we're only clearing depth
depthStencilAttachment: depthAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
_prepareBuffer(renderBuffer, geometry, shader) {
const attr = shader.attributes[renderBuffer.attr];
if (!attr) return;
const { src, dst, size, map } = renderBuffer;
const device = this.device;
const buffers = this._getOrMakeCachedBuffers(geometry);
let srcData = geometry[src];
if (!srcData || srcData.length === 0) {
if (renderBuffer.default) {
srcData = geometry[src] = renderBuffer.default(geometry);
srcData.isDefault = true;
} else {
return;
}
}
// Check if we already have a buffer for this data
let existingBuffer = buffers[dst];
const needsNewBuffer = !existingBuffer;
// Only create new buffer and write data if buffer doesn't exist or data is dirty
if (needsNewBuffer || geometry.dirtyFlags[src] !== false) {
const raw = map ? map(srcData) : srcData;
const typed = this._normalizeBufferData(raw, Float32Array);
// Get pooled buffer (may reuse existing or create new)
const pooledBufferInfo = this._getVertexBufferFromPool(geometry, dst, typed.byteLength);
// Create a copy of the data to avoid conflicts when geometry arrays are reset
const dataCopy = new typed.constructor(typed);
pooledBufferInfo.dataCopy = dataCopy;
// Write the data to the pooled buffer
device.queue.writeBuffer(pooledBufferInfo.buffer, 0, dataCopy);
// Update the buffers cache to use the pooled buffer
buffers[dst] = pooledBufferInfo.buffer;
geometry.dirtyFlags[src] = false;
}
shader.enableAttrib(attr, size);
}
_disableRemainingAttributes(shader) {}
_enableAttrib(attr) {
// TODO: is this necessary?
const loc = attr.location;
if (!this.registerEnabled.has(loc)) {
// TODO
this.registerEnabled.add(loc);
}
}
_ensureGeometryBuffers(buffers, indices, indexType) {
if (!indices) return;
const device = this.device;
const buffer = device.createBuffer({
size: Math.ceil((indices.length * indexType.BYTES_PER_ELEMENT) / 4) * 4,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
// Write index data to buffer
const mapping = new indexType(buffer.getMappedRange());
mapping.set(indices);
buffer.unmap();
buffers.indexBuffer = buffer;
buffers.indexBufferType = indexType === Uint32Array ? 'uint32' : 'uint16';
}
_freeBuffers(buffers) {
const destroyIfExists = (buf) => {
if (buf && buf.destroy) {
buf.destroy();
}
};
destroyIfExists(buffers.indexBuffer);
const freeDefs = (defs) => {
for (const def of defs) {
destroyIfExists(buffers[def.dst]);
buffers[def.dst] = null;
}
};
freeDefs(this.buffers.stroke);
freeDefs(this.buffers.fill);
freeDefs(this.buffers.user);
}
_getValidSampleCount(requestedCount) {
// WebGPU supports sample counts of 1, 4 (and sometimes 8)
if (requestedCount <= 1) return 1;
if (requestedCount <= 4) return 4;
return 4; // Cap at 4 for broader compatibility
}
_shaderOptions({ mode, compute, workgroupSize }) {
if (compute) return { compute: true, workgroupSize };
const activeFramebuffer = this.activeFramebuffer();
const format = activeFramebuffer ?
this._getWebGPUColorFormat(activeFramebuffer) :
this.presentationFormat;
const requestedSampleCount = activeFramebuffer ?
(activeFramebuffer.antialias ? activeFramebuffer.antialiasSamples : 1) :
1; // No MSAA needed when blitting already-antialiased textures to canvas
const sampleCount = this._getValidSampleCount(requestedSampleCount);
const depthFormat = activeFramebuffer
? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined)
: this.depthFormat;
const drawTarget = this.drawTarget();
const clipping = this._clipping;
const clipApplied = drawTarget._isClipApplied;
return {
topology: mode === TRIANGLE_STRIP ? 'triangle-strip' : 'triangle-list',
blendMode: this.states.curBlendMode,
sampleCount,
format,
depthFormat,
clipping,
clipApplied,
}
}
_shaderOptionsDifferent(newOptions) {
if (!this.activeShaderOptions) return true;
for (const key in this.activeShaderOptions) {
if (this.activeShaderOptions[key] !== newOptions[key]) return true;
}
return false;
}
_initShader(shader) {
const device = this.device;
if (shader.shaderType === 'compute') {
// Compute shader initialization
shader.computeModule = device.createShaderModule({ code: shader.computeSrc() });
shader._computePipelineCache = null;
shader._workgroupSize = null;
// Create compute pipeline (deferred until first compute() call)
shader.getPipeline = ({ workgroupSize }) => {
if (!shader._computePipelineCache) {
shader._computePipelineCache = device.createComputePipeline({
layout: shader._pipelineLayout,
compute: {
module: shader.computeModule,
entryPoint: 'main'
}
});
shader._workgroupSize = workgroupSize;
}
return shader._computePipelineCache;
};
return;
}
// Render shader initialization
shader.vertModule = device.createShaderModule({ code: shader.vertSrc() });
shader.fragModule = device.createShaderModule({ code: shader.fragSrc() });
shader._pipelineCache = new Map();
shader.getPipeline = ({ topology, blendMode, sampleCount, format, depthFormat, clipping, clipApplied }) => {
const key = `${topology}_${blendMode}_${sampleCount}_${format}_${depthFormat}_${clipping}_${clipApplied}`;
if (!shader._pipelineCache.has(key)) {
const pipeline = device.createRenderPipeline({
layout: shader._pipelineLayout,
vertex: {
module: shader.vertModule,
entryPoint: 'main',
buffers: this._getVertexLayout(shader),
},
fragment: {
module: shader.fragModule,
entryPoint: 'main',
targets: [{
format,
blend: this._getBlendState(blendMode),
}],
},
primitive: { topology },
multisample: { count: sampleCount },
...(depthFormat ? {
depthStencil: {
format: depthFormat,
depthWriteEnabled: !clipping,
depthCompare: 'less-equal',
stencilFront: {
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
failOp: 'keep',
depthFailOp: 'keep',
passOp: clipping ? 'replace' : 'keep',
},
stencilBack: {
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
failOp: 'keep',
depthFailOp: 'keep',
passOp: clipping ? 'replace' : 'keep',
},
stencilReadMask: 0xFF,
stencilWriteMask: clipping ? 0xFF : 0x00,
},
} : {}),
});
shader._pipelineCache.set(key, pipeline);
}
return shader._pipelineCache.get(key);
};
}
_finalizeShader(shader) {
// Per-group buffer pools. We will pull from these when we draw multiple
// times using the shader in a render pass. These are per group instead of
// global so that we can reuse the last used buffer when uniform values
// don't change.
shader._uniformBufferGroups = [];
shader.buffersDirty = new Set();
for (const group of shader._uniformGroups) {
// Calculate the size needed for this group's uniforms
const groupUniforms = Object.values(group.uniforms);
const rawSize = Math.max(
0,
...groupUniforms.map(u => u.offsetEnd)
);
const alignedSize = Math.ceil(rawSize / 16) * 16;
shader._uniformBufferGroups.push({
group: group.group,
binding: group.binding,
cacheKey: group.group * 1000 + group.binding,
varName: group.varName,
structType: group.structType,
uniforms: groupUniforms,
size: alignedSize,
bufferPool: [],
nextBufferPool: [],
dynamic: groupUniforms.some(u => u.name.startsWith('uModel')),
buffersInUse: new Set(),
currentBuffer: null, // For caching
});
}
// Register this shader in our registry for pool cleanup
this._shadersWithPools.push(shader);
const bindGroupLayouts = new Map(); // group index -> bindGroupLayout
const groupEntries = new Map(); // group index -> array of entries
// Add all uniform group bindings to group 0
const structEntries = new Map();
for (const bufferGroup of shader._uniformBufferGroups) {
const entries = structEntries.get(bufferGroup.group) || [];
entries.push({
bufferGroup,
binding: bufferGroup.binding,
visibility: shader.shaderType === 'compute'
? GPUShaderStage.COMPUTE
: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
});
structEntries.set(bufferGroup.group, entries);
}
for (const [group, entries] of structEntries.entries()) {
entries.sort((a, b) => a.binding - b.binding);
groupEntries.set(group, entries);
}
// Add the variable amount of samplers and texture bindings that can come after
for (const sampler of shader.samplers) {
const group = sampler.group;
const entries = groupEntries.get(group) || [];
if (!['sampler', 'texture_2d<f32>'].includes(sampler.type)) {
throw new Error(`Unsupported texture type: ${sampler.type}`);
}
entries.push({
binding: sampler.binding,
visibility: sampler.visibility,
sampler: sampler.type === 'sampler'
? { type: 'filtering' }
: undefined,
texture: sampler.type === 'texture_2d<f32>'
? { sampleType: 'float', viewDimension: '2d' }
: undefined,
uniform: sampler,
});
entries.sort((a, b) => a.binding - b.binding);
groupEntries.set(group, entries);
}
// Add storage buffer bindings
for (const storage of shader._storageBuffers || []) {
const group = storage.group;
const entries = groupEntries.get(group) || [];
entries.push({
binding: storage.binding,
visibility: storage.visibility,
buffer: {
type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage'
},
storage: storage,
});
entries.sort((a, b) => a.binding - b.binding);
groupEntries.set(group, entries);
}
// Create layouts and bind groups
const groupEntriesArr = [];
for (const [group, entries] of groupEntries) {
const layout = this.device.createBindGroupLayout({ entries });
bindGroupLayouts.set(group, layout);
groupEntriesArr.push([group, entries]);
}
shader._groupEntries = groupEntriesArr;
shader._bindGroupLayouts = [...bindGroupLayouts.values()];
// Reuse bind groups if they don't change
shader._cachedBindGroup = {};
// Remember which dynamic buffer we last used, so that we can
// possibly cache bind groups if unchanged
shader._lastDynamicBuffer = {};
shader._pipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: shader._bindGroupLayouts,
});
shader._compiled = true;
}
_getBlendState(mode) {
switch (mode) {
case BLEND:
return {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha'
}
};
case ADD:
return {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one'
}
};
case REMOVE:
return {
color: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha'
},
alpha: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha'
}
};
case MULTIPLY:
return {
color: {
operation: 'add',
srcFactor: 'dst-color',
dstFactor: 'one-minus-src-alpha'
},
alpha: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'one-minus-src-alpha'
}
};
case SCREEN:
return {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-color'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha'
}
};
case EXCLUSION:
return {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-color',
dstFactor: 'one-minus-src-color'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one'
}
};
case REPLACE:
return {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'zero'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'zero'
}
};
case SUBTRACT:
return {
color: {
operation: 'reverse-subtract',
srcFactor: 'one',
dstFactor: 'one'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha'
}
};
case DARKEST:
return {
color: {
operation: 'min',
srcFactor: 'one',
dstFactor: 'one'
},
alpha: {
operation: 'min',
srcFactor: 'one',
dstFactor: 'one'
}
};
case LIGHTEST:
return {
color: {
operation: 'max',
srcFactor: 'one',
dstFactor: 'one'
},
alpha: {
operation: 'max',
srcFactor: 'one',
dstFactor: 'one'
}
};
default:
console.warn(`Unsupported blend mode: ${mode}`);
return undefined;
}
}
_applyColorBlend() {}
_getVertexLayout(shader) {
const layouts = [];
for (const attrName in shader.attributes) {
const attr = shader.attributes[attrName];
if (!attr || attr.location === -1) continue;
// Get the vertex buffer info associated with this attribute
const renderBuffer =
this.buffers[shader.shaderType].find(buf => buf.attr === attrName) ||
this.buffers.user.find(buf => buf.attr === attrName);
if (!renderBuffer) continue;
const { size } = renderBuffer;
// Convert from the number of floats (e.g. 3) to a recognized WebGPU
// format (e.g. "float32x3")
const format = this._getFormatFromSize(size);
layouts.push({
arrayStride: size * 4,
stepMode: 'vertex',
attributes: [
{
shaderLocation: attr.location,
offset: 0,
format,
},
],
});
}
return layouts;
}
_getVertexBuffers(shader) {
if (!shader._vertexBuffers) {
const buffers = [];
for (const attrName in shader.attributes) {
const attr = shader.attributes[attrName];
if (!attr || attr.location === -1) continue;
// Get the vertex buffer info associated with this attribute
const renderBuffer =
this.buffers[shader.shaderType].find(buf => buf.attr === attrName) ||
this.buffers.user.find(buf => buf.attr === attrName);
if (!renderBuffer) continue;
buffers.push(renderBuffer);
}
shader._vertexBuffers = buffers;
}
return shader._vertexBuffers;
}
_getFormatFromSize(size) {
switch (size) {
case 1: return 'float32';
case 2: return 'float32x2';
case 3: return 'float32x3';
case 4: return 'float32x4';
default: throw new Error(`Unsupported attribute size: ${size}`);
}
}
_useShader(shader, options) {}
_updateViewport() {
this._origViewport = {
width: this.width,
height: this.height,
};
this._viewport = [0, 0, this.width, this.height];
}
_createPixelsArray() {
this.pixels = new Uint8Array(
this.width * this.pixelDensity() * this.height * this.pixelDensity() * 4
);
}
supportsTriangleFan() {
return false;
}
viewport() {}
zClipRange() {
return [0, 1];
}
defaultNearScale() {
return 0.01;
}
defaultFarScale() {
return 100;
}
_resetBuffersBeforeDraw() {
this._finishActiveRenderPass();
// Set state to PENDING - we'll decide on first draw
if (this._pInst.frameCount > 0) {
this._frameState = FRAME_STATE.PENDING;
}
// Clear depth buffer but DON'T start any render pass yet
const activeFramebuffer = this.activeFramebuffer();
const commandEncoder = this.device.createCommandEncoder();
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
if (depthTextureView) {
const depthAttachment = {
view: depthTextureView,
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
stencilLoadOp: 'load',
stencilStoreOp: 'store',
};
const renderPassDescriptor = {
colorAttachments: [],
depthStencilAttachment: depthAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
}
/**
* Promotes the current frame to use mainFramebuffer.
* Copies current canvas content to mainFramebuffer, then switches to rendering there.
* @private
*/
_promoteToFramebuffer() {
// Already promoted this frame
if (this._frameState === FRAME_STATE.PROMOTED) {
return;
}
// Already drawing to a custom framebuffer, no promotion needed
if (this.activeFramebuffer()) {
return;
}
// Flush any pending draws to canvas first
this.flushDraw();
// Mark as promoted
this._frameState = FRAME_STATE.PROMOTED;
// Get current canvas texture
const canvasTexture = this.drawingContext.getCurrentTexture();
// Ensure mainFramebuffer matches canvas size
if (this.mainFramebuffer.width !== this.width ||
this.mainFramebuffer.height !== this.height) {
this.mainFramebuffer.resize(this.width, this.height);
}
// Copy canvas textures to mainFramebuffer
const commandEncoder = this.device.createCommandEncoder();
// Copy color texture
commandEncoder.copyTextureToTexture(
{
texture: canvasTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
},
{
texture: this.mainFramebuffer.colorTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
},
{
width: Math.ceil(this.width * this._pixelDensity),
height: Math.ceil(this.height * this._pixelDensity),
depthOrArrayLayers: 1,
}
);
// Copy depth texture
commandEncoder.copyTextureToTexture(
{
texture: this.depthTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
},
{
texture: this.mainFramebuffer.depthTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
},
{
width: Math.ceil(this.width * this._pixelDensity),
height: Math.ceil(this.height * this._pixelDensity),
depthOrArrayLayers: 1,
}
);
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
// We want to make sure the transformation state is the same
// once we're drawing to the framebuffer, because normally
// those are reset.
const savedModelMatrix = this.states.uModelMatrix.copy();
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
this.states.uModelMatrix.reset();
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
this.mainFramebuffer.begin();
this.states.uModelMatrix.set(savedModelMatrix);
}
_promoteToFramebufferWithoutCopy() {
// Already promoted this frame
if (this._frameState === FRAME_STATE.PROMOTED) {
return;
}
// Ensure mainFramebuffer matches canvas size
if (this.mainFramebuffer.width !== this.width ||
this.mainFramebuffer.height !== this.height) {
this.mainFramebuffer.resize(this.width, this.height);
}
// Mark as promoted WITHOUT copying canvas content
this._frameState = FRAME_STATE.PROMOTED;
// Flush any pending draws first
this.flushDraw();
// Preserve transformation state
const savedModelMatrix = this.states.uModelMatrix.copy();
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
this.states.uModelMatrix.reset();
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
// Begin rendering to mainFramebuffer
this.mainFramebuffer.begin();
this.states.uModelMatrix.set(savedModelMatrix);
}
//////////////////////////////////////////////
// Geometry buffer pool management
//////////////////////////////////////////////
_initializeGeometryBufferPools(geometry) {
if (geometry._vertexBufferPools) {
return; // Already initialized
}
geometry._vertexBufferPools = {}; // Keyed by buffer type (dst)
geometry._vertexBuffersInUse = {}; // Keyed by buffer type (dst)
geometry._vertexBuffersToReturn = {}; // Keyed by buffer type (dst)
// Register this geometry for pool cleanup
this._geometriesWithPools.push(geometry);
}
_getVertexBufferFromPool(geometry, dst, size) {
// Initialize pools if needed
this._initializeGeometryBufferPools(geometry);
// Get or create pool for this buffer type
if (!geometry._vertexBufferPools[dst]) {
geometry._vertexBufferPools[dst] = [];
}
if (!geometry._vertexBuffersInUse[dst]) {
geometry._vertexBuffersInUse[dst] = [];
}
if (!geometry._vertexBuffersToReturn[dst]) {
geometry._vertexBuffersToReturn[dst] = [];
}
// Try to get a buffer from the pool
const pool = geometry._vertexBufferPools[dst];
if (pool.length > 0) {
const bufferInfo = pool.pop();
// Check if buffer is large enough
if (bufferInfo.buffer.size >= size) {
geometry._vertexBuffersInUse[dst].push(bufferInfo);
return bufferInfo;
} else {
// Buffer too small, don't destroy immediately as it may still be in use
// Add to retirement array
this._retiredBuffers.push(bufferInfo.buffer);
}
}
// No suitable buffer available, create a new one
const newBuffer = this.device.createBuffer({
size,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
const bufferInfo = {
buffer: newBuffer,
size,
// Create a copy of the data array to avoid conflicts when geometry is reset
dataCopy: null
};
geometry._vertexBuffersInUse[dst].push(bufferInfo);
return bufferInfo;
}
_returnVertexBuffersToPool() {
// Return buffers marked for return back to their pools for all registered geometries
for (const geometry of this._geometriesWithPools) {
if (geometry._vertexBuffersToReturn) {
for (const [dst, buffersToReturn] of Object.entries(geometry._vertexBuffersToReturn)) {
if (buffersToReturn.length > 0) {
// Move all buffers from ToReturn back to pool
const pool = geometry._vertexBufferPools[dst] || [];
while (buffersToReturn.length > 0) {
const bufferInfo = buffersToReturn.pop();
// Clear the data copy reference to prevent memory leaks
bufferInfo.dataCopy = null;
pool.push(bufferInfo);
}
geometry._vertexBufferPools[dst] = pool;
}
}
}
}
}
// Called when geometry is reset - mark its buffers for return
onReset(geometry) {
this._markGeometryBuffersForReturn(geometry);
}
// Mark geometry buffers for return when geometry is reset/freed
_markGeometryBuffersForReturn(geometry) {
if (geometry._vertexBuffersInUse && geometry._vertexBuffersToReturn) {
for (const [dst, buffersInUse] of Object.entries(geometry._vertexBuffersInUse)) {
if (buffersInUse.length > 0) {
// Move all buffers from InUse to ToReturn
const buffersToReturn = geometry._vertexBuffersToReturn[dst] || [];
while (buffersInUse.length > 0) {
const bufferInfo = buffersInUse.pop();
buffersToReturn.push(bufferInfo);
}
geometry._vertexBuffersToReturn[dst] = buffersToReturn;
}
}
}
}
//////////////////////////////////////////////
// Uniform buffer pool management
//////////////////////////////////////////////
_getUniformBufferFromPool(bufferGroup) {
// Try to get a buffer from the pool
if (bufferGroup.bufferPool.length > 0) {
const bufferInfo = bufferGroup.bufferPool.pop();
bufferGroup.buffersInUse.add(bufferInfo);
return bufferInfo;
}
// No buffers available, create a new one
const newBuffer = this.device.createBuffer({
size: bufferGroup.size,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const newData = new Float32Array(bufferGroup.size / 4);
const newDataView = new DataView(newData.buffer);
const bufferInfo = {
buffer: newBuffer,
data: newData,
dataView: newDataView
};
bufferGroup.buffersInUse.add(bufferInfo);
return bufferInfo;
}
_getDynamicUniformBufferFromPool(bufferGroup) {
//
let buffer;
if (
this.currentUniformBuffer &&
this.currentUniformBuffer.offset + bufferGroup.size < this.currentUniformBuffer.size
) {
// We can fit this next block of uniforms into the current active memory chunk
buffer = this.currentUniformBuffer;
} else if (this.uniformBufferPool.length > 0) {
buffer = this.uniformBufferPool.pop();
this.activeUniformBuffers.push(buffer);
} else {
// Kinda arbitrary. Each dynamic offset has to be in groups of 256, but then
// we can choose how many things we want to be able to fit into a block.
// There's some overhead to each block so if we're drawing a lot of stuff,
// bigger is better. But it's also a lot of wasted memory if we AREN'T drawing
// a lot of stuff. So.... right now it's 40. Feel free to update this if
// a better balance can be achieved.
const size = 256 * 40;
buffer = {
dynamic: true,
lastOffset: 0,
offset: 0,
size,
buffer: this.device.createBuffer({
size,
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
}),
uniformBuffer: this.device.createBuffer({
size,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
}),
};
buffer.data = new Float32Array(buffer.buffer.getMappedRange());
buffer.dataView = new DataView(buffer.data.buffer);
this.activeUniformBuffers.push(buffer);
}
this.currentUniformBuffer = buffer;
return buffer;
}
_returnUniformBuffersToPool() {
// Return all used buffers back to their pools for all registered shaders
for (const shader of this._shadersWithPools) {
this._returnShaderBuffersToPool(shader);
}
}
_returnShaderBuffersToPool(shader) {
if (shader._uniformBufferGroups) {
for (const bufferGroup of shader._uniformBufferGroups) {
while (bufferGroup.nextBufferPool.length > 0) {
bufferGroup.bufferPool.push(bufferGroup.nextBufferPool.pop());
}
for (const bufferInfo of bufferGroup.buffersInUse.keys()) {
if (bufferInfo !== bufferGroup.currentBuffer) {
bufferGroup.nextBufferPool.push(bufferInfo);
}
}
bufferGroup.buffersInUse.clear();
if (bufferGroup.currentBuffer) {
bufferGroup.buffersInUse.add(bufferGroup.currentBuffer);
}
}
}
}
flushDraw() {
this._finishActiveRenderPass();
// Only submit if we actually had any draws
if (this._hasPendingDraws) {
// Create a copy of pending command encoders
const commandsToSubmit = this._pendingCommandEncoders;
this._pendingCommandEncoders = [];
this._hasPendingDraws = false;
if (this.activeUniformBuffers.length > 0) {
const encoder = this.device.createCommandEncoder();
for (const bufferInfo of this.activeUniformBuffers) {
bufferInfo.buffer.unmap();
encoder.copyBufferToBuffer(
bufferInfo.buffer,
bufferInfo.uniformBuffer,
);
}
commandsToSubmit.unshift(encoder.finish());
}
// Submit the commands
this.queue.submit(commandsToSubmit);
for (const buf of this.activeUniformBuffers) {
// buf.buffer = this.device.createBuffer({
// size: buf.size,
// usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
// mappedAtCreation: true,
// });
buf.offset = 0;
buf.lastOffset = 0;
// this.resettingUniformBuffers.push(
buf.buffer.mapAsync(GPUMapMode.WRITE).then(() => {
buf.data = new Float32Array(buf.buffer.getMappedRange());
buf.dataView = new DataView(buf.data.buffer);
this.uniformBufferPool.push(buf);
return buf;
});
// )
}
this.activeUniformBuffers = [];
this.currentUniformBuffer = undefined;
// Execute post-submit callbacks after GPU work completes
if (this._postSubmitCallbacks.length > 0) {
const callbacks = this._postSubmitCallbacks;
this._postSubmitCallbacks = [];
this.device.queue.onSubmittedWorkDone().then(() => {
for (const callback of callbacks) {
callback();
}
});
}
// Reset canvas texture cache for next frame
this.currentCanvasColorTexture = null;
this.currentCanvasColorTextureView = null;
}
}
_ensurePixelReadCanvas(width, height) {
// Create canvas if it doesn't exist
if (!this._pixelReadCanvas) {
this._pixelReadCanvas = document.createElement('canvas');
this._pixelReadCtx = this._pixelReadCanvas.getContext('2d');
}
// Resize canvas if dimensions changed
if (this._pixelReadCanvas.width !== width || this._pixelReadCanvas.height !== height) {
this._pixelReadCanvas.width = width;
this._pixelReadCanvas.height = height;
}
return { canvas: this._pixelReadCanvas, ctx: this._pixelReadCtx };
}
resize(w, h) {
super.resize(w, h);
this._hasPendingDraws = true;
this.flushDraw();
}
async finishDraw() {
this.flushDraw();
const states = [];
// Only blit if we promoted to framebuffer this frame
if (this._frameState === FRAME_STATE.PROMOTED) {
while (this.activeFramebuffers.length > 0) {
const fbo = this.activeFramebuffers.pop();
states.unshift({ fbo, diff: { ...this.states } });
}
this.flushDraw();
this._pInst.push();
this.states.setValue('enableLighting', false);
this.states.setValue('activeImageLight', null);
this._pInst.setCamera(this.finalCamera);
this._pInst.shader(this._getBlitShader());
this._pInst.resetMatrix();
this._pInst.imageMode(this._pInst.CENTER);
this._pInst.image(this.mainFramebuffer, 0, 0);
this._pInst.pop();
this.flushDraw();
}
// Return all uniform buffers to their pools
this._returnUniformBuffersToPool();
// Mark all geometry buffers for return after frame is complete
for (const geometry of this._geometriesWithPools) {
this._markGeometryBuffersForReturn(geometry);
}
// this.uniformBufferPool.push(...(await Promise.all(this.resettingUniformBuffers)));
this.resettingUniformBuffers = [];
// Return all vertex buffers to their pools
this._returnVertexBuffersToPool();
// Destroy all retired buffers
const retired = this._retiredBuffers;
this._postSubmitCallbacks.push(() => {
for (const buffer of retired) {
if (buffer && buffer.destroy) {
buffer.destroy();
}
}
});
this._retiredBuffers = [];
if (this._frameState === FRAME_STATE.PROMOTED) {
for (const { fbo, diff } of states) {
if (fbo !== this.mainFramebuffer || this._frameState !== FRAME_STATE.PROMOTED) {
fbo.begin();
}
for (const key in diff) {
this.states.setValue(key, diff[key]);
}
}
}
}
//////////////////////////////////////////////
// Rendering
//////////////////////////////////////////////
_drawBuffers(geometry, { mode = TRIANGLES, count = 1 }) {
const buffers = this.geometryBufferCache.getCached(geometry);
if (!buffers) return;
// If PENDING and no custom framebuffer, regular draw means PROMOTE
if (this._frameState === FRAME_STATE.PENDING && !this.activeFramebuffer()) {
this._promoteToFramebufferWithoutCopy();
}
this._beginActiveRenderPass();
const passEncoder = this.activeRenderPass;
const currentShader = this._curShader;
this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers });
// Bind vertex buffers
for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
const location = currentShader.attributes[buffer.attr].location;
const gpuBuffer = buffers[buffer.dst];
passEncoder.setVertexBuffer(location, gpuBuffer, 0);
}
if (currentShader.shaderType === "fill") {
// Bind index buffer and issue draw
if (buffers.indexBuffer) {
const indexFormat = buffers.indexFormat || "uint16";
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
} else {
passEncoder.draw(geometry.vertices.length, count, 0, 0);
}
} else if (currentShader.shaderType === "text") {
if (!buffers.indexBuffer) {
throw new Error("Text geometry must have an index buffer");
}
const indexFormat = buffers.indexFormat || "uint16";
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
}
if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
}
// Mark that we have pending draws that need submission
this._hasPendingDraws = true;
}
setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) {
const shaderOptions = this._shaderOptions(shaderOptionsParams);
if (
shaderOptions.compute ||
this.activeShader !== currentShader ||
this._shaderOptionsDifferent(shaderOptions)
) {
passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
}
if (!shaderOptions.compute) {
this.activeShader = currentShader;
this.activeShaderOptions = shaderOptions;
// Set stencil reference value for clipping
const drawTarget = this.drawTarget();
if (drawTarget._isClipApplied && !this._clipping) {
// When using the clip mask, test against reference value 0 (background)
// WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
// In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
passEncoder.setStencilReference(0);
} else if (this._clipping) {
// When writing to the clip mask, write reference value 1
passEncoder.setStencilReference(1);
}
}
for (const bufferGroup of currentShader._uniformBufferGroups) {
if (bufferGroup.dynamic) {
// Bind uniforms into a part of a big dynamic memory block because
// the group changes often
const uniformBufferInfo = this._getDynamicUniformBufferFromPool(bufferGroup);
if (currentShader._lastDynamicBuffer[bufferGroup.cacheKey] !== uniformBufferInfo) {
currentShader._cachedBindGroup[bufferGroup.group] = undefined;
currentShader._lastDynamicBuffer[bufferGroup.cacheKey] = uniformBufferInfo;
}
this._packUniformGroup(currentShader, bufferGroup.uniforms, uniformBufferInfo);
uniformBufferInfo.lastOffset = uniformBufferInfo.offset;
uniformBufferInfo.offset += Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment;
// Make a shallow copy so that we keep track of the last offset for this uniform
bufferGroup.currentDynamicBuffer = uniformBufferInfo;
bufferGroup.lastOffset = uniformBufferInfo.lastOffset;
} else {
// Bind uniforms to a binding-specific buffer, which may be cached for performance
let bufferInfo;
const dataChanged = this._hasGroupDataChanged(currentShader, bufferGroup);
if (!dataChanged && bufferGroup.currentBuffer) {
// Reuse the cached buffer - no need to pack or write
bufferInfo = bufferGroup.currentBuffer;
bufferGroup.buffersInUse.add(bufferInfo);
} else {
// Data changed - get a new buffer and write to it
bufferInfo = this._getUniformBufferFromPool(bufferGroup);
this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo);
this.device.queue.writeBuffer(
bufferInfo.buffer,
0,
bufferInfo.data.buffer,
bufferInfo.data.byteOffset,
bufferInfo.data.byteLength
);
currentShader.buffersDirty.delete(bufferGroup.group * 1000 + bufferGroup.binding);
currentShader._cachedBindGroup[bufferGroup.group] = undefined;
// Cache this buffer and data for next frame
bufferGroup.currentBuffer = bufferInfo;
}
}
}
for (const sampler of currentShader.samplers) {
const key = sampler.group * 1000 + sampler.binding;
if (currentShader.buffersDirty.has(key)) {
currentShader._cachedBindGroup[sampler.group] = undefined;
currentShader.buffersDirty.delete(key);
}
}
for (const storage of currentShader._storageBuffers || []) {
const key = storage.group * 1000 + storage.binding;
if (currentShader.buffersDirty.has(key)) {
currentShader._cachedBindGroup[storage.group] = undefined;
currentShader.buffersDirty.delete(key);
}
}
// Bind sampler/texture uniforms and uniform buffers
for (const iter of currentShader._groupEntries) {
const group = iter[0];
const entries = iter[1];
let dynamicOffsetIdx = 0;
const bgEntries = [];
let bindGroup = currentShader._cachedBindGroup[group];
for (const entry of entries) {
const bufferGroup = entry.bufferGroup;
// Check if this is a uniform buffer binding
const uniformBufferInfo =
bufferGroup?.currentBuffer || bufferGroup?.currentDynamicBuffer;
if (uniformBufferInfo) {
if (bufferGroup.dynamic) {
this.dynamicEntryOffsets[dynamicOffsetIdx++] = bufferGroup.lastOffset;
}
if (!bindGroup) {
bgEntries.push({
binding: entry.binding,
resource: bufferGroup.dynamic
? {
buffer: uniformBufferInfo.uniformBuffer,
offset: 0,
size: Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment,
}
: { buffer: uniformBufferInfo.buffer },
});
}
} else if (entry.storage && !bindGroup) {
// Storage buffer binding
const uniform = currentShader.uniforms[entry.storage.name];
if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) {
throw new Error(
`Storage buffer "${entry.storage.name}" not set. ` +
`Use shader.setUniform("${entry.storage.name}", storageBuffer)`
);
}
bgEntries.push({
binding: entry.binding,
resource: { buffer: uniform._cachedData.buffer },
});
} else if (!bindGroup) {
bgEntries.push({
binding: entry.binding,
resource: entry.uniform.type === 'sampler'
? (entry.uniform.textureSource.texture || this._getEmptyTexture()).getSampler()
: (entry.uniform.texture || this._getEmptyTexture()).textureHandle.view,
});
}
}
const layout = currentShader._bindGroupLayouts[group];
if (!bindGroup) {
bindGroup = this.device.createBindGroup({
layout,
entries: bgEntries,
});
}
currentShader._cachedBindGroup[group] = bindGroup;
if (dynamicOffsetIdx === 0) {
passEncoder.setBindGroup(
group,
bindGroup,
);
} else {
passEncoder.setBindGroup(
group,
bindGroup,
this.dynamicEntryOffsets,
0,
dynamicOffsetIdx
);
}
}
return passEncoder;
}
//////////////////////////////////////////////
// SHADER
//////////////////////////////////////////////
// Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset).
//
// Field interface (shared by uniform fields from _parseStruct and struct storage schema fields):
// baseType: string - 'f32', 'i32', 'u32', etc.
// size: number - byte size of the field
// offset: number - byte offset of the field within its struct
// packInPlace: bool - true for mat3, written with manual column padding
//
// value: number or number[] - the data to write
_packField(field, value, floatView, dataView, baseOffset) {
if (value === undefined) return;
// Duck typing instead of instanceof to avoid importing a separate
// copy of the Color/Vector classes
if (value?.isVector) {
value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values;
} else if (value?.isColor) {
value = value._getRGBA([1, 1, 1, 1]);
}
const byteOffset = baseOffset + field.offset;
if (field.baseType === 'u32') {
if (field.size === 4) {
dataView.setUint32(byteOffset, value, true);
} else {
for (let i = 0; i < value.length; i++) {
dataView.setUint32(byteOffset + i * 4, value[i], true);
}
}
} else if (field.baseType === 'i32') {
if (field.size === 4) {
dataView.setInt32(byteOffset, value, true);
} else {
for (let i = 0; i < value.length; i++) {
dataView.setInt32(byteOffset + i * 4, value[i], true);
}
}
} else if (field.packInPlace) {
// In-place packing for mat3: write directly to buffer with padding
const base = byteOffset / 4;
floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2];
floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5];
floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8];
} else if (field.size === 4) {
floatView.set([value], byteOffset / 4);
} else {
floatView.set(value, byteOffset / 4);
}
}
_packUniformGroup(shader, groupUniforms, bufferInfo) {
// Pack a single group's uniforms into a buffer
const data = bufferInfo.data;
const dataView = bufferInfo.dataView;
const offset = bufferInfo.offset || 0;
for (const uniform of groupUniforms) {
const fullUniform = shader.uniforms[uniform.name];
if (!fullUniform || fullUniform.isSampler) continue;
this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset);
}
}
_hasGroupDataChanged(shader, bufferGroup) {
// First time
if (!bufferGroup.currentBuffer) return true;
return shader.buffersDirty.has(bufferGroup.group * 1000 + bufferGroup.binding);
}
_parseStruct(shaderSource, structName) {
const structMatch = shaderSource.match(
new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]+)\\}`)
);
if (!structMatch) {
throw new Error(`Can't find a struct definition for ${structName}`);
}
const structBody = structMatch[1];
const elements = {};
let match;
let index = 0;
let offset = 0;
const elementRegex =
/(?:@location\((\d+)\)\s+)?(\w+):\s*([^\n]+?),?\n/g;
const baseAlignAndSize = (type) => {
if (['f32', 'i32', 'u32', 'bool'].includes(type)) {
return { align: 4, size: 4, items: 1, baseType: type };
}
if (/^vec[2-4](<f32>|f)$/.test(type)) {
const n = parseInt(type.match(/^vec([2-4])/)[1]);
const size = 4 * n;
const align = n === 2 ? 8 : 16;
return { align, size, items: n, baseType: 'f32' };
}
if (/^vec[2-4]<(i32|u32)>$/.test(type)) {
const n = parseInt(type.match(/^vec([2-4])/)[1]);
const match = type.match(/^vec[2-4]<(i32|u32)>$/);
const baseType = match[1]; // 'i32' or 'u32'
const size = 4 * n;
const align = n === 2 ? 8 : 16;
return { align, size, items: n, baseType };
}
if (/^mat[2-4](?:x[2-4])?(<f32>|f)$/.test(type)) {
if (type[4] === 'x' && type[3] !== type[5]) {
throw new Error('Non-square matrices not implemented yet');
}
const dim = parseInt(type[3]);
const align = dim === 2 ? 8 : 16;
// Each column must be aligned
const size = Math.ceil(dim * 4 / align) * align * dim;
// For mat3, use in-place packing to avoid array allocation
const pack = dim === 3
? (data) => [
...data.slice(0, 3),
...data.slice(3, 6),
...data.slice(6, 9),
]
: undefined;
const packInPlace = dim === 3;
return { align, size, pack, packInPlace, items: dim * dim, baseType: 'f32' };
}
if (/^array<.+>$/.test(type)) {
const [, subtype, rawLength] = type.match(/^array<(.+),\s*(\d+)>/);
const length = parseInt(rawLength);
const {
align: elemAlign,
size: elemSize,
items: elemItems,
pack: elemPack = (data) => [...data],
baseType: elemBaseType
} = baseAlignAndSize(subtype);
const stride = Math.ceil(elemSize / elemAlign) * elemAlign;
const pack = (data) => {
const result = [];
for (let i = 0; i < data.length; i += elemItems) {
const elemData = elemPack(data.slice(i, elemItems));
result.push(...elemData);
for (let j = 0; j < stride / 4 - elemData.length; j++) {
result.push(0);
}
}
return result;
};
return {
align: elemAlign,
size: stride * length,
items: elemItems * length,
pack,
baseType: elemBaseType
};
}
throw new Error(`Unknown type in WGSL struct: ${type}`);
};
while ((match = elementRegex.exec(structBody)) !== null) {
const [_, location, name, type] = match;
const { size, align, pack, packInPlace, baseType } = baseAlignAndSize(type);
offset = Math.ceil(offset / align) * align;
const offsetEnd = offset + size;
elements[name] = {
name,
location: location ? parseInt(location) : undefined,
index,
type,
size,
offset,
offsetEnd,
pack,
packInPlace,
baseType
};
index++;
offset = offsetEnd;
}
return elements;
}
_mapUniformData(uniform, data) {
if (uniform.pack) {
return uniform.pack(data);
}
return data;
}
_getShaderAttributes(shader) {
const mainMatch = /fn main\(.+:\s*([^\s\)]+)/.exec(shader._vertSrc);
if (!mainMatch) throw new Error("Can't find `fn main` in vertex shader source");
const inputType = mainMatch[1];
return this._parseStruct(shader.vertSrc(), inputType);
}
getUniformMetadata(shader) {
// Parse all uniform struct bindings in group 0.
// TODO: support non-sampler uniforms being in other groups
// Each binding represents a logical group of uniforms, since they get
// updated or cached all at once.
const uniformGroups = [];
const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
let match;
const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc();
while ((match = uniformVarRegex.exec(src)) !== null) {
const [_, groupNum, binding, varName, structType] = match;
const bindingIndex = parseInt(binding);
const uniforms = this._parseStruct(src, structType);
uniformGroups.push({
group: parseInt(groupNum),
binding: bindingIndex,
varName,
structType,
uniforms
});
}
if (uniformGroups.length === 0 && shader.shaderType !== 'compute') {
throw new Error('Expected at least one uniform struct bound to @group(0)');
}
// While we're also keeping track of the groups, the API we expose
// to users of p5 is just a flat list of uniforms (which can be the
// individual struct items in the group.)
const allUniforms = {};
for (const group of uniformGroups) {
for (const [uniformName, uniformData] of Object.entries(group.uniforms)) {
allUniforms[uniformName] = {
...uniformData,
group: group.group,
binding: group.binding,
varName: group.varName
};
}
}
// Store uniform groups for buffer pooling
shader._uniformGroups = uniformGroups;
// Extract samplers from group bindings
const samplers = {};
// TODO: support other texture types
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
// Extract storage buffers
const storageBuffers = {};
const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g;
// Track which bindings are taken by the struct properties we've parsed
// (the rest should be textures/samplers)
const structUniformBindings = {};
for (const g of uniformGroups) {
structUniformBindings[g.group + ',' + g.binding] = true;
}
for (const [src, visibility] of [
[shader.vertSrc(), GPUShaderStage.VERTEX],
[shader.fragSrc(), GPUShaderStage.FRAGMENT],
[shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE]
]) {
if (!src) continue; // Skip if shader stage doesn't exist
let match;
while ((match = samplerRegex.exec(src)) !== null) {
const [_, group, binding, name, type] = match;
const groupIndex = parseInt(group);
const bindingIndex = parseInt(binding);
// Skip struct uniform bindings which we've already parsed
if (structUniformBindings[groupIndex + ',' + bindingIndex]) continue;
const key = `${groupIndex},${bindingIndex}`;
samplers[key] = {
visibility: (samplers[key]?.visibility || 0) | visibility,
group: groupIndex,
binding: bindingIndex,
name,
type,
isSampler: true,
noData: type === 'sampler',
};
}
for (const sampler of Object.values(samplers)) {
if (sampler.type.startsWith('texture')) {
const samplerName = sampler.name + '_sampler';
const samplerNode = Object
.values(samplers)
.find((s) => s.name === samplerName);
if (!samplerNode) {
throw new Error(
`Every shader texture needs an accompanying sampler. Could not find sampler ${samplerName} for texture ${sampler.name}`
);
}
samplerNode.textureSource = sampler;
}
}
// Parse storage buffers
while ((match = storageRegex.exec(src)) !== null) {
const [_, group, binding, accessMode, name] = match;
const groupIndex = parseInt(group);
const bindingIndex = parseInt(binding);
const key = `${groupIndex},${bindingIndex}`;
const existing = storageBuffers[key];
// If any stage uses read_write, the bind group layout must use read_write
const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write')
? 'read_write'
: accessMode;
storageBuffers[key] = {
visibility: (existing?.visibility || 0) | visibility,
group: groupIndex,
binding: bindingIndex,
name,
accessMode: finalAccessMode, // 'read' or 'read_write'
isStorage: true,
type: 'storage'
};
}
}
// Store storage buffers on shader for later use
shader._storageBuffers = Object.values(storageBuffers);
return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)];
}
getNextBindingIndex({ vert, frag, compute }, group = 0) {
// Get the highest binding index in the specified group and return the next available
const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g;
let maxBindingIndex = -1;
const sources = [];
if (vert) sources.push([vert, GPUShaderStage.VERTEX]);
if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]);
if (compute) sources.push([compute, GPUShaderStage.COMPUTE]);
for (const [src, visibility] of sources) {
let match;
while ((match = bindingRegex.exec(src)) !== null) {
const [_, groupIndex, bindingIndex] = match;
if (parseInt(groupIndex) === group) {
maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
}
}
}
return maxBindingIndex + 1;
}
updateUniformValue(shader, uniform, data) {
if (uniform.isSampler) {
uniform.texture =
data instanceof Texture ? data : this.getTexture(data);
} else if (!data?._isStorageBuffer) {
uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
}
shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
}
_updateTexture(uniform, tex) {
tex.update();
}
bindTexture(tex) {}
unbindTexture(tex) {}
_unbindFramebufferTexture(uniform) {}
createTexture({ width, height, format = 'rgba8unorm', usage }) {
const gpuTexture = this.device.createTexture({
size: [width, height],
format,
usage: usage || (
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT
),
});
return { gpuTexture, view: gpuTexture.createView() };
}
uploadTextureFromSource({ gpuTexture }, source) {
this.queue.copyExternalImageToTexture(
{ source },
{ texture: gpuTexture },
[source.width, source.height]
);
// Force submission to ensure texture upload completes before usage
this._hasPendingDraws = true;
this.flushDraw();
}
uploadTextureFromData({ gpuTexture }, data, width, height) {
this.queue.writeTexture(
{ texture: gpuTexture },
data,
{ bytesPerRow: width * 4, rowsPerImage: height },
{ width, height, depthOrArrayLayers: 1 }
);
// Force submission to ensure texture upload completes before usage
this._hasPendingDraws = true;
this.flushDraw();
}
setTextureParams(_texture) {}
getSampler(texture) {
const key = `${texture.minFilter}_${texture.magFilter}_${texture.wrapS}_${texture.wrapT}`;
if (this.samplers.has(key)) {
return this.samplers.get(key);
}
const constantMapping = {
[NEAREST]: 'nearest',
[LINEAR]: 'linear',
[CLAMP]: 'clamp-to-edge',
[REPEAT]: 'repeat',
[MIRROR]: 'mirror-repeat'
};
const sampler = this.device.createSampler({
magFilter: constantMapping[texture.magFilter],
minFilter: constantMapping[texture.minFilter],
addressModeU: constantMapping[texture.wrapS],
addressModeV: constantMapping[texture.wrapT],
});
this.samplers.set(key, sampler);
return sampler;
}
bindTextureToShader(_texture, _sampler, _uniformName, _unit) {}
deleteTexture({ gpuTexture }) {
this._postSubmitCallbacks.push(() => gpuTexture.destroy());
}
_getLightShader() {
if (!this._defaultLightShader) {
this._defaultLightShader = new Shader(
this,
materialVertexShader,
materialFragmentShader,
{
vertex: {
"void beforeVertex": "() {}",
"Vertex getObjectInputs": "(inputs: Vertex) { return inputs; }",
"Vertex getWorldInputs": "(inputs: Vertex) { return inputs; }",
"Vertex getCameraInputs": "(inputs: Vertex) { return inputs; }",
"void afterVertex": "() {}",
},
fragment: {
"void beforeFragment": "() {}",
"Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
"vec4f combineColors": `(components: ColorComponents) {
var rgb = vec3<f32>(0.0);
rgb += components.diffuse * components.baseColor;
rgb += components.ambient * components.ambientColor;
rgb += components.specular * components.specularColor;
rgb += components.emissive;
return vec4<f32>(rgb, components.opacity);
}`,
"vec4f getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
"void afterFragment": "() {}",
},
}
);
}
return this._defaultLightShader;
}
_getColorShader() {
if (!this._defaultColorShader) {
this._defaultColorShader = new Shader(
this,
colorVertexShader,
colorFragmentShader,
{
vertex: {
"void beforeVertex": "() {}",
"Vertex getObjectInputs": "(inputs: Vertex) { return inputs; }",
"Vertex getWorldInputs": "(inputs: Vertex) { return inputs; }",
"Vertex getCameraInputs": "(inputs: Vertex) { return inputs; }",
"void afterVertex": "() {}",
},
fragment: {
"void beforeFragment": "() {}",
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
"void afterFragment": "() {}",
},
}
);
}
return this._defaultColorShader;
}
_getLineShader() {
if (!this._defaultLineShader) {
this._defaultLineShader = new Shader(
this,
lineDefs + lineVertexShader,
lineDefs + lineFragmentShader,
{
vertex: {
"void beforeVertex": "() {}",
"StrokeVertex getObjectInputs": "(inputs: StrokeVertex) { return inputs; }",
"StrokeVertex getWorldInputs": "(inputs: StrokeVertex) { return inputs; }",
"StrokeVertex getCameraInputs": "(inputs: StrokeVertex) { return inputs; }",
"void afterVertex": "() {}",
},
fragment: {
"void beforeFragment": "() {}",
"Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
"bool shouldDiscard": "(outside: bool) { return outside; };",
"void afterFragment": "() {}",
},
}
);
}
return this._defaultLineShader;
}
_getFontShader() {
if (!this._defaultFontShader) {
this._defaultFontShader = new Shader(
this,
fontVertexShader,
fontFragmentShader
);
}
return this._defaultFontShader;
}
_getBlitShader() {
if (!this._defaultBlitShader) {
this._defaultBlitShader = new Shader(
this,
blitVertexShader,
blitFragmentShader
);
}
return this._defaultBlitShader;
}
//////////////////////////////////////////////
// Setting
//////////////////////////////////////////////
_adjustDimensions(width, height) {
// TODO: find max texture size
return { adjustedWidth: width, adjustedHeight: height };
}
_applyClip() {
const commandEncoder = this.device.createCommandEncoder();
const activeFramebuffer = this.activeFramebuffer();
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
if (!depthTextureView) {
return;
}
const depthStencilAttachment = {
view: depthTextureView,
stencilLoadOp: 'clear',
stencilStoreOp: 'store',
stencilClearValue: 0,
depthReadOnly: true,
stencilReadOnly: false,
};
const renderPassDescriptor = {
colorAttachments: [],
depthStencilAttachment: depthStencilAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
_unapplyClip() {
// In WebGPU, clip unapplication is handled through pipeline state rather than direct commands
// The stencil test configuration is set in the render pipeline based on _clipping and _clipInvert flags
// This is already handled in the _shaderOptions() method and pipeline creation
}
_clearClipBuffer() {
this._finishActiveRenderPass();
const commandEncoder = this.device.createCommandEncoder();
const activeFramebuffer = this.activeFramebuffer();
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
if (!depthTextureView) {
return;
}
const depthStencilAttachment = {
view: depthTextureView,
stencilLoadOp: 'clear',
stencilStoreOp: 'store',
stencilClearValue: 1,
depthReadOnly: true,
stencilReadOnly: false,
};
const renderPassDescriptor = {
colorAttachments: [],
depthStencilAttachment: depthStencilAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
_applyStencilTestIfClipping() {
// This is done via pipeline state in WebGL so this is a no-op
}
//////////////////////////////////////////////
// Shader hooks
//////////////////////////////////////////////
uniformNameFromHookKey(key) {
return key.slice(0, key.indexOf(':'));
}
populateHooks(shader, src, shaderType) {
if (!src.includes('fn main')) return src;
// Apply some p5-specific preprocessing. WGSL doesn't have preprocessor
// statements, but some of our shaders might need it, so we add a lightweight
// way to add code if a hook is augmented. e.g.:
// struct Uniforms {
// // @p5 ifdef Vertex getWorldInputs
// uModelMatrix: mat4f,
// uViewMatrix: mat4f,
// // @p5 endif
// // @p5 ifndef Vertex getWorldInputs
// uModelViewMatrix: mat4f,
// // @p5 endif
// }
src = src.replace(
/\/\/ @p5 (ifdef|ifndef) (\w+)\s+(\w+)\n((?:(?!\/\/ @p5)(?:.|\n))*)\/\/ @p5 endif/g,
(_, condition, hookType, hookName, body) => {
const target = condition === 'ifdef';
if (
(
!!shader.hooks.modified.vertex[`${hookType} ${hookName}`] ||
!!shader.hooks.modified.fragment[`${hookType} ${hookName}`]
) === target
) {
return body;
} else {
return '';
}
}
);
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/);
const getBuiltinParamName = (mainSrc, builtinName) => {
const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc);
return match ? match[1] : null;
};
const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => {
const existingName = getBuiltinParamName(mainSrc, builtinName);
if (existingName) {
return { mainSrc, argName: existingName };
}
const hasParams = /\(\s*\S/.test(mainSrc);
const injectedMain = mainSrc.replace(
/\)\s*(->|\{)/,
`${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1`
);
return { mainSrc: injectedMain, argName: fallbackName };
};
const getMainStructParameter = (mainSrc) => {
const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc);
if (!match) return null;
return { inputName: match[1], structName: match[2] };
};
const getStructBuiltinFieldName = (structName, builtinName) => {
const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain);
if (!structMatch) return null;
const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]);
return fieldMatch ? fieldMatch[1] : null;
};
const appendHookParams = (params, additionalParams) => {
if (additionalParams.length === 0) return params;
const hasParams = !/^\(\s*\)$/.test(params);
return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`;
};
let hookExtraParams = [];
let hookExtraArgs = [];
if (shaderType === 'vertex') {
const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32');
main = ensuredInstance.mainSrc;
const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32');
main = ensuredVertex.mainSrc;
hookExtraParams = ['instanceID: u32', '_p5VertexId: u32'];
hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName];
} else if (shaderType === 'fragment') {
const directPositionArg = getBuiltinParamName(main, 'position');
let fragmentPositionArg = directPositionArg;
if (!fragmentPositionArg) {
const mainStructParam = getMainStructParameter(main);
if (mainStructParam) {
const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position');
if (positionField) {
fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`;
}
}
}
if (!fragmentPositionArg) {
const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4<f32>');
main = ensuredPosition.mainSrc;
fragmentPositionArg = ensuredPosition.argName;
}
hookExtraParams = ['_p5FragPos: vec4<f32>'];
hookExtraArgs = [fragmentPositionArg];
} else if (shaderType === 'compute') {
const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3<u32>');
main = ensuredGlobalId.mainSrc;
hookExtraParams = ['_p5GlobalId: vec3<u32>'];
hookExtraArgs = [ensuredGlobalId.argName];
}
// Inject hook uniforms as a separate struct at a new binding
let hookUniformFields = '';
for (const key in shader.hooks.uniforms) {
// Skip textures, they don't get added to structs
if (key.endsWith(': sampler2D')) continue;
// WGSL format: "name: type"
hookUniformFields += ` ${key},\n`;
}
if (hookUniformFields) {
// Find the next available binding in group 0
// Use the source we're currently building (preMain) which has texture bindings. We can't call `fragSrc()`
// or `vertSrc()` because we may be in one of those calls already, and might infinite loop
const nextBinding = this.getNextBindingIndex({
vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc,
}, 0);
// Create HookUniforms struct and binding
const hookUniformsDecl = `
// Hook Uniforms (from .modify())
struct HookUniforms {
${hookUniformFields}}
@group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
`;
// Insert before the first @group binding, or at the end if there are none
const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
if (replaced === preMain) {
// No @group bindings found in base shader, append to preMain
preMain = preMain + '\n' + hookUniformsDecl;
} else {
preMain = replaced;
}
}
// Handle varying variables by injecting them into VertexOutput and FragmentInput structs
if (shader.hooks.varyingVariables && shader.hooks.varyingVariables.length > 0) {
// Generate struct members for varying variables
let nextLocationIndex = this._getNextAvailableLocation(preMain, shaderType);
let varyingMembers = '';
for (const varyingVar of shader.hooks.varyingVariables) {
// varyingVar is a string like "varName: vec3<f32>"
const member = `@location(${nextLocationIndex++}) ${varyingVar},`;
varyingMembers += member + '\n';
}
if (shaderType === 'vertex') {
// Inject into VertexOutput struct
preMain = preMain.replace(
/struct\s+VertexOutput\s+\{([^}]*)\}/,
(match, body) => `struct VertexOutput {${body}\n${varyingMembers}}`
);
} else if (shaderType === 'fragment') {
// Inject into FragmentInput struct
preMain = preMain.replace(
/struct\s+FragmentInput\s+\{([^}]*)\}/,
(match, body) => `struct FragmentInput {${body}\n${varyingMembers}}`
);
}
}
// Add file-global varying variable declarations
if (shader.hooks.varyingVariables && shader.hooks.varyingVariables.length > 0) {
let varyingDeclarations = '';
for (const varyingVar of shader.hooks.varyingVariables) {
// varyingVar is a string like "varName: vec3<f32>"
const [varName, varType] = varyingVar.split(':').map(s => s.trim());
varyingDeclarations += `var<private> ${varName}: ${varType};\n`;
}
// Add declarations before the main function
preMain += varyingDeclarations;
if (shaderType === 'vertex') {
// In vertex shader, copy varying variables to output struct before return
let copyStatements = '';
for (const varyingVar of shader.hooks.varyingVariables) {
const [varName] = varyingVar.split(':').map(s => s.trim());
copyStatements += ` OUTPUT_VAR.${varName} = ${varName};\n`;
}
// Find the output variable name from the return statement and replace OUTPUT_VAR
const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
if (returnMatch) {
const outputVarName = returnMatch[1];
copyStatements = copyStatements.replace(/OUTPUT_VAR/g, outputVarName);
// Insert before the return statement
postMain = postMain.replace(/(return\s+\w+\s*;)/g, `${copyStatements} $1`);
}
} else if (shaderType === 'fragment') {
// In fragment shader, initialize varying variables from input struct at start of main
let initStatements = '';
for (const varyingVar of shader.hooks.varyingVariables) {
const [varName] = varyingVar.split(':').map(s => s.trim());
initStatements += ` ${varName} = INPUT_VAR.${varName};\n`;
}
const mainStructParam = getMainStructParameter(main);
if (mainStructParam) {
const inputVarName = mainStructParam.inputName;
initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName);
// Insert after the main function parameter but before any other code (anchored to start)
postMain = initStatements + postMain;
}
}
}
// Handle instanceID varying for fragment access
if (shader.hooks.instanceIDVarying) {
const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying;
const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType);
const interpAttr = interpolation ? ` @interpolate(${interpolation})` : '';
const [varName, varType] = declaration.split(':').map(s => s.trim());
const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`;
if (shaderType === 'vertex') {
// Inject into VertexOutput struct
preMain = preMain.replace(
/struct\s+VertexOutput\s+\{([^}]*)\}/,
(match, body) => `struct VertexOutput {${body}\n${structMember}}`
);
// Add private global
preMain += `var<private> ${declaration};\n`;
// Assign from built-in instanceID at start of main()
postMain = `\n ${varName} = ${source};\n` + postMain;
// Copy to output struct before return
const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
if (returnMatch) {
const outputVarName = returnMatch[1];
postMain = postMain.replace(
/(return\s+\w+\s*;)/g,
`${outputVarName}.${varName} = ${varName};\n $1`
);
}
} else if (shaderType === 'fragment') {
// Inject into FragmentInput struct
preMain = preMain.replace(
/struct\s+FragmentInput\s+\{([^}]*)\}/,
(match, body) => `struct FragmentInput {${body}\n${structMember}}`
);
// Add private global
preMain += `var<private> ${declaration};\n`;
// Initialize from input struct at start of main()
const mainStructParam = getMainStructParameter(main);
if (mainStructParam) {
const inputVarName = mainStructParam.inputName;
postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain;
}
}
}
let hooks = '';
let defines = '';
if (shader.hooks.declarations) {
hooks += shader.hooks.declarations + '\n';
}
if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) {
hooks += shader.hooks[shaderType].declarations + '\n';
}
for (const hookDef in shader.hooks.helpers) {
const [hookType, hookName] = hookDef.split(' ');
const [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks.helpers[hookDef]);
if (hookType === 'void') {
hooks += `fn ${hookName}${params}${body}\n`;
} else {
hooks += `fn ${hookName}${params} -> ${hookType}${body}\n`;
}
}
for (const hookDef in shader.hooks[shaderType]) {
if (hookDef === 'declarations') continue;
const [hookType, hookName] = hookDef.split(' ');
// Add a const so that if the shader wants to
// optimize away the extra function calls in main, it can do so
defines += `const AUGMENTED_HOOK_${hookName} = ${
shader.hooks.modified[shaderType][hookDef] ? 'true' : 'false'
};\n`;
let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]);
params = appendHookParams(params, hookExtraParams);
if (hookType === 'void') {
hooks += `fn HOOK_${hookName}${params}${body}\n`;
} else {
hooks += `fn HOOK_${hookName}${params} -> ${hookType}${body}\n`;
}
}
// Pass stage-specific builtins from main to each hook call.
// Collect ALL HOOK_ calls (including nested ones) then insert
// extra args from right to left so position shifts don't
// invalidate earlier insertion points.
if (hookExtraArgs.length > 0) {
const addHookArgs = (src) => {
const insertions = [];
let searchIdx = 0;
let m;
while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) {
const openParen = searchIdx + m.index + m[0].length - 1;
let pos = openParen + 1;
let nesting = 1;
let hasParams = false;
while (pos < src.length && nesting > 0) {
if (src[pos] === '(') nesting++;
else if (src[pos] === ')') {
nesting--;
if (nesting === 0) break;
} else if (/\S/.test(src[pos])) {
hasParams = true;
}
pos++;
}
insertions.push({ pos, hasParams });
searchIdx = openParen + 1;
}
insertions.sort((a, b) => b.pos - a.pos);
let result = src;
for (const { pos, hasParams } of insertions) {
const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', ');
result = result.slice(0, pos) + insertion + result.slice(pos);
}
return result;
};
preMain = addHookArgs(preMain);
postMain = addHookArgs(postMain);
}
return preMain + '\n' + defines + hooks + main + postMain;
}
_getNextAvailableLocation(shaderSource, shaderType) {
// Parse existing struct to find the highest @location number
let maxLocation = -1;
const structName = shaderType === 'vertex' ? 'VertexOutput' : 'FragmentInput';
// Find the struct definition
const structMatch = shaderSource.match(new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's'));
if (structMatch) {
const structBody = structMatch[1];
// Find all @location(N) declarations
const locationMatches = structBody.matchAll(/@location\((\d+)\)/g);
for (const match of locationMatches) {
const locationNum = parseInt(match[1]);
if (locationNum > maxLocation) {
maxLocation = locationNum;
}
}
}
return maxLocation + 1;
}
getShaderHookTypes(shader, hookName) {
// Create mapping from WGSL types to DataType entries
const wgslToDataType = {
'f32': DataType.float1,
'vec2<f32>': DataType.float2,
'vec3<f32>': DataType.float3,
'vec4<f32>': DataType.float4,
'vec2f': DataType.float2,
'vec3f': DataType.float3,
'vec4f': DataType.float4,
'i32': DataType.int1,
'vec2<i32>': DataType.int2,
'vec3<i32>': DataType.int3,
'vec4<i32>': DataType.int4,
'bool': DataType.bool1,
'vec2<bool>': DataType.bool2,
'vec3<bool>': DataType.bool3,
'vec4<bool>': DataType.bool4,
'mat2x2<f32>': DataType.mat2,
'mat3x3<f32>': DataType.mat3,
'mat4x4<f32>': DataType.mat4,
'texture_2d<f32>': DataType.sampler2D
};
let fullSrc = shader._vertSrc;
let body = shader.hooks.vertex[hookName];
if (!body) {
body = shader.hooks.fragment[hookName];
fullSrc = shader._fragSrc;
}
if (!body) {
body = shader.hooks.compute[hookName];
fullSrc = shader._computeSrc;
}
if (!body) {
throw new Error(`Can't find hook ${hookName}!`);
}
const nameParts = hookName.split(/\s+/g);
const functionName = nameParts.pop();
const returnType = nameParts.pop();
const returnQualifiers = [...nameParts];
const parameterMatch = /\(([^\)]*)\)/.exec(body);
if (!parameterMatch) {
throw new Error(`Couldn't find function parameters in hook body:\n${body}`);
}
const structProperties = structName => {
// WGSL struct parsing: struct StructName { field1: Type, field2: Type }
const structDefMatch = new RegExp(`struct\\s+${structName}\\s*{([^}]*)}`).exec(fullSrc);
if (!structDefMatch) return undefined;
const properties = [];
// Parse WGSL struct fields (e.g., "texCoord: vec2<f32>,")
for (const fieldSrc of structDefMatch[1].split(',')) {
const trimmed = fieldSrc.trim();
if (!trimmed) continue;
// Remove location decorations and parse field
// Format: [@location(N)] fieldName: Type
const fieldMatch = /(?:@location\([^)]*\)\s*)?(\w+)\s*:\s*([^,\s]+)/.exec(trimmed);
if (!fieldMatch) continue;
const name = fieldMatch[1];
let typeName = fieldMatch[2];
const dataType = wgslToDataType[typeName] || null;
const typeProperties = structProperties(typeName);
properties.push({
name,
type: {
typeName: typeName, // Keep native WGSL type name
qualifiers: [],
properties: typeProperties,
dataType: dataType
}
});
}
return properties;
};
const parameters = parameterMatch[1].split(',').map(paramString => {
// WGSL function parameters: name: type or name: binding<type>
const trimmed = paramString.trim();
if (!trimmed) return null;
const parts = trimmed.split(':').map(s => s.trim());
if (parts.length !== 2) return null;
const name = parts[0];
let typeName = parts[1];
// Handle texture bindings like "texture_2d<f32>" -> sampler2D DataType
if (typeName.includes('texture_2d')) {
typeName = 'texture_2d<f32>';
}
const dataType = wgslToDataType[typeName] || null;
const properties = structProperties(typeName);
return {
name,
type: {
typeName: typeName, // Keep native WGSL type name
qualifiers: [],
properties,
dataType: dataType
}
};
}).filter(Boolean);
// Convert WGSL return type to DataType
const returnDataType = wgslToDataType[returnType] || null;
return {
name: functionName,
returnType: {
typeName: returnType, // Keep native WGSL type name
qualifiers: returnQualifiers,
properties: structProperties(returnType),
dataType: returnDataType
},
parameters
};
}
//////////////////////////////////////////////
// Buffer management for pixel reading
//////////////////////////////////////////////
_ensurePixelReadBuffer(requiredSize) {
// Create or resize staging buffer if needed
if (!this.pixelReadBuffer || this.pixelReadBufferSize < requiredSize) {
// Clean up old buffer
if (this.pixelReadBuffer) {
this.flushDraw();
this.pixelReadBuffer.destroy();
}
// Create new buffer with padding to avoid frequent recreations
// Scale by 2 to ensure integer size and reasonable headroom
const bufferSize = Math.max(requiredSize, this.pixelReadBufferSize * 2);
this.pixelReadBuffer = this.device.createBuffer({
size: bufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
this.pixelReadBufferSize = bufferSize;
}
return this.pixelReadBuffer;
}
_alignBytesPerRow(bytesPerRow) {
// WebGPU requires bytesPerRow to be a multiple of 256 bytes for texture-to-buffer copies
return Math.ceil(bytesPerRow / 256) * 256;
}
//////////////////////////////////////////////
// Framebuffer methods
//////////////////////////////////////////////
defaultFramebufferAlpha() {
return true
}
defaultFramebufferAntialias() {
return this._pInst._webgpuAttributes?.antialias !== false;
}
supportsFramebufferAntialias() {
return true;
}
createFramebufferResources(framebuffer) {
}
validateFramebufferFormats(framebuffer) {
if (![
UNSIGNED_BYTE,
FLOAT,
HALF_FLOAT
].includes(framebuffer.format)) {
console.warn(
'Unknown Framebuffer format. ' +
'Please use UNSIGNED_BYTE, FLOAT, or HALF_FLOAT. ' +
'Defaulting to UNSIGNED_BYTE.'
);
framebuffer.format = UNSIGNED_BYTE;
}
if (framebuffer.useDepth && ![
UNSIGNED_INT,
FLOAT
].includes(framebuffer.depthFormat)) {
console.warn(
'Unknown Framebuffer depth format. ' +
'Please use UNSIGNED_INT or FLOAT. Defaulting to FLOAT.'
);
framebuffer.depthFormat = FLOAT;
}
}
recreateFramebufferTextures(framebuffer) {
this.flushDraw();
// Clean up existing textures
if (framebuffer.colorTexture && framebuffer.colorTexture.destroy) {
const tex = framebuffer.colorTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
framebuffer.colorTextureView = null;
}
if (framebuffer.aaColorTexture && framebuffer.aaColorTexture.destroy) {
const tex = framebuffer.aaColorTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
framebuffer.aaColorTextureView = null;
}
if (framebuffer.depthTexture && framebuffer.depthTexture.destroy) {
const tex = framebuffer.depthTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
framebuffer.depthTextureView = null;
}
if (framebuffer.aaDepthTexture && framebuffer.aaDepthTexture.destroy) {
const tex = framebuffer.aaDepthTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
framebuffer.aaDepthTextureView = null;
}
const baseDescriptor = {
size: {
width: framebuffer.width * framebuffer.density,
height: framebuffer.height * framebuffer.density,
depthOrArrayLayers: 1,
},
format: this._getWebGPUColorFormat(framebuffer),
};
// Create non-multisampled texture for texture binding (always needed)
const colorTextureDescriptor = {
...baseDescriptor,
usage: GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_SRC |
(framebuffer._useCanvasFormat ? GPUTextureUsage.COPY_DST : 0),
sampleCount: 1,
};
framebuffer.colorTexture = this.device.createTexture(colorTextureDescriptor);
framebuffer.colorTextureView = framebuffer.colorTexture.createView();
// Create multisampled texture for rendering if antialiasing is enabled
if (framebuffer.antialias) {
const aaColorTextureDescriptor = {
...baseDescriptor,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
sampleCount: this._getValidSampleCount(framebuffer.antialiasSamples),
};
framebuffer.aaColorTexture = this.device.createTexture(aaColorTextureDescriptor);
framebuffer.aaColorTextureView = framebuffer.aaColorTexture.createView();
}
if (framebuffer.useDepth) {
const depthBaseDescriptor = {
size: {
width: framebuffer.width * framebuffer.density,
height: framebuffer.height * framebuffer.density,
depthOrArrayLayers: 1,
},
format: this._getWebGPUDepthFormat(framebuffer),
};
// Create non-multisampled depth texture for texture binding (always needed)
const depthTextureDescriptor = {
...depthBaseDescriptor,
usage: GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.TEXTURE_BINDING |
(framebuffer._useCanvasFormat ? GPUTextureUsage.COPY_DST : 0),
sampleCount: 1,
};
framebuffer.depthTexture = this.device.createTexture(depthTextureDescriptor);
framebuffer.depthTextureView = framebuffer.depthTexture.createView();
// Create multisampled depth texture for rendering if antialiasing is enabled
if (framebuffer.antialias) {
const aaDepthTextureDescriptor = {
...depthBaseDescriptor,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
sampleCount: this._getValidSampleCount(framebuffer.antialiasSamples),
};
framebuffer.aaDepthTexture = this.device.createTexture(aaDepthTextureDescriptor);
framebuffer.aaDepthTextureView = framebuffer.aaDepthTexture.createView();
}
}
// Clear the framebuffer textures after creation
this._clearFramebufferTextures(framebuffer);
}
_clearFramebufferTextures(framebuffer) {
this._finishActiveRenderPass();
const commandEncoder = this.device.createCommandEncoder();
// Clear the color texture (and multisampled texture if it exists)
const colorAttachment = {
view: framebuffer.aaColorTexture
? framebuffer.aaColorTextureView
: framebuffer.colorTextureView,
loadOp: "clear",
storeOp: "store",
clearValue: { r: 0, g: 0, b: 0, a: 0 },
resolveTarget: framebuffer.aaColorTexture
? framebuffer.colorTextureView
: undefined,
};
// Clear the depth texture if it exists
const depthTexture = framebuffer.aaDepthTexture || framebuffer.depthTexture;
const depthStencilAttachment = depthTexture ? {
view: framebuffer.aaDepthTexture
? framebuffer.aaDepthTextureView
: framebuffer.depthTextureView,
depthLoadOp: "clear",
depthStoreOp: "store",
depthClearValue: 1.0,
stencilLoadOp: "clear",
stencilStoreOp: "store",
depthReadOnly: false,
stencilReadOnly: false,
} : undefined;
const renderPassDescriptor = {
colorAttachments: [colorAttachment],
depthStencilAttachment: depthStencilAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
_getFramebufferColorTextureView(framebuffer) {
if (framebuffer.colorTexture) {
return framebuffer.colorTextureView;
}
return null;
}
createFramebufferTextureHandle(framebufferTexture) {
const src = framebufferTexture;
let renderer = this;
return {
get view() {
return renderer._getFramebufferColorTextureView(src.framebuffer);
},
get gpuTexture() {
return src.framebuffer.colorTexture;
}
};
}
// Maps a plain JS value to the WGSL type string that represents it in a struct.
_jsValueToWgslType(value) {
if (typeof value === 'number') return 'f32';
// Duck typing instead of instanceof to avoid importing a separate
// copy of the Color/Vector classes
if (value?.isVector) {
if (value.dimensions === 2) return 'vec2f';
if (value.dimensions === 3) return 'vec3f';
if (value.dimensions === 4) return 'vec4f';
throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`);
}
if (value?.isColor) {
return 'vec4f';
}
if (Array.isArray(value)) {
if (value.length === 2) return 'vec2f';
if (value.length === 3) return 'vec3f';
if (value.length === 4) return 'vec4f';
throw new Error(`Unsupported array length ${value.length} for struct storage field`);
}
throw new Error(`Unsupported value type ${typeof value} for struct storage field`);
}
// Infers a struct schema from the first element of a struct array.
//
// Returns { fields, stride, structBody } where:
// fields: field has the _packField interface (baseType, size, offset, packInPlace) plus:
// name: string - JS property name
// dim: number - float component count, used when creating StrandsNodes
// structBody: everything inside the { ... } of a WGSL struct definition
// stride: how many bytes are reserved for this struct in the buffer
_inferStructSchema(firstElement) {
const entries = Object.entries(firstElement);
if (!p5.disableFriendlyErrors) {
for (const [name, value] of entries) {
if (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
// Duck typing instead of instanceof to avoid importing a separate
// copy of the Color/Vector classes
!value?.isVector &&
!value?.isColor
) {
p5._friendlyError(
`The "${name}" property in your storage data contains a nested object. ` +
`Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`,
'createStorage'
);
}
}
}
const fieldLines = entries.map(([name, value]) =>
` ${name}: ${this._jsValueToWgslType(value)},`
).join('\n');
const structBody = `{\n${fieldLines}\n}`;
const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp');
let maxEnd = 0;
let maxAlign = 1;
const fields = entries.map(([name, value]) => {
const el = elements[name];
maxEnd = Math.max(maxEnd, el.offsetEnd);
// Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
maxAlign = Math.max(maxAlign, align);
// Track original JS type for reconstruction during readback
const kind = value?.isVector ? 'vector'
: value?.isColor ? 'color'
: undefined;
return {
name,
baseType: el.baseType,
size: el.size,
offset: el.offset,
packInPlace: el.packInPlace ?? false,
dim: el.size / 4,
kind,
};
});
const stride = Math.ceil(maxEnd / maxAlign) * maxAlign;
return { fields, stride, structBody };
}
// Packs an array of plain objects into a Float32Array using the given struct schema.
// Reuses _packField so layout rules match uniform packing exactly.
_packStructArray(data, schema) {
const { fields, stride } = schema;
const totalBytes = Math.max(data.length * stride, 16);
const alignedBytes = Math.ceil(totalBytes / 16) * 16;
const buffer = new ArrayBuffer(alignedBytes);
const floatView = new Float32Array(buffer);
const dataView = new DataView(buffer);
for (let i = 0; i < data.length; i++) {
const item = data[i];
const baseOffset = i * stride;
for (const field of fields) {
this._packField(field, item[field.name], floatView, dataView, baseOffset);
}
}
return floatView;
}
// Inverse of _packStructArray reads packed buffer back into plain JS objects
// using the same schema layout - fields, stride and offsets
_unpackStructArray(floatView, schema) {
const { fields, stride } = schema;
const dataView = new DataView(floatView.buffer);
const count = Math.floor(floatView.byteLength / stride);
const result = [];
for (let i = 0; i < count; i++) {
const item = {};
const baseOffset = i * stride;
for (const field of fields) {
const byteOffset = baseOffset + field.offset;
const n = field.size / 4;
if (field.baseType === 'u32') {
if (n === 1) {
item[field.name] = dataView.getUint32(byteOffset, true);
} else {
item[field.name] = Array.from({ length: n }, (_, j) =>
dataView.getUint32(byteOffset + j * 4, true)
);
}
} else if (field.baseType === 'i32') {
if (n === 1) {
item[field.name] = dataView.getInt32(byteOffset, true);
} else {
item[field.name] = Array.from({ length: n }, (_, j) =>
dataView.getInt32(byteOffset + j * 4, true)
);
}
} else {
const idx = byteOffset / 4;
if (n === 1) {
item[field.name] = floatView[idx];
} else {
const values = Array.from(floatView.slice(idx, idx + n));
if (field.kind === 'vector') {
item[field.name] = this._pInst.createVector(...values);
} else if (field.kind === 'color') {
// Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
// Scale back to the current colorMode range
const maxes = this.states.colorMaxes[this.states.colorMode];
item[field.name] = this._pInst.color(
values[0] * maxes[0], values[1] * maxes[1],
values[2] * maxes[2], values[3] * maxes[3]
);
} else {
item[field.name] = values;
}
}
}
}
result.push(item);
}
return result;
}
createStorage(dataOrCount) {
const device = this.device;
// Struct array: an array of plain objects
if (Array.isArray(dataOrCount) && dataOrCount.length > 0 &&
typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) {
if (!p5.disableFriendlyErrors && dataOrCount.length > 1) {
const firstKeys = Object.keys(dataOrCount[0]);
let warned = false;
for (let i = 1; i < dataOrCount.length; i++) {
const el = dataOrCount[i];
const elKeys = Object.keys(el);
const sameKeys = firstKeys.length === elKeys.length &&
firstKeys.every((k, j) => k === elKeys[j]);
if (!sameKeys) {
p5._friendlyError(
`Element ${i} has different fields than element 0. ` +
`All elements should have the same properties.`,
'createStorage'
);
break;
}
for (const key of firstKeys) {
const firstType = this._jsValueToWgslType(dataOrCount[0][key]);
const elType = this._jsValueToWgslType(el[key]);
if (firstType !== elType) {
p5._friendlyError(
`The "${key}" property of element ${i} has type ${elType} ` +
`but element 0 has type ${firstType}. Proporties should have the same type across all elements.`,
'createStorage'
);
warned = true;
break;
}
}
if (warned) break;
}
}
const schema = this._inferStructSchema(dataOrCount[0]);
const packed = this._packStructArray(dataOrCount, schema);
const size = packed.byteLength;
const buffer = device.createBuffer({
size,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
new Float32Array(buffer.getMappedRange()).set(packed);
buffer.unmap();
const storageBuffer = new StorageBuffer(buffer, size, this, schema);
this._storageBuffers.add(storageBuffer);
return storageBuffer;
}
// Determine buffer size and initial data
let size, initialData;
if (typeof dataOrCount === 'number') {
// createStorage(count) - zero-initialized
size = dataOrCount * 4; // floats are 4 bytes
initialData = new Float32Array(dataOrCount);
} else {
// createStorage(array) - from data
if (dataOrCount instanceof Float32Array) {
initialData = dataOrCount;
} else if (Array.isArray(dataOrCount)) {
initialData = new Float32Array(dataOrCount);
} else {
throw new Error('createStorage expects a number or array/Float32Array');
}
size = initialData.byteLength;
}
// Align to 16 bytes (WGSL storage buffer alignment requirement)
size = Math.ceil(size / 16) * 16;
// Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage
const buffer = device.createBuffer({
size,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
mappedAtCreation: initialData.length > 0
});
// Write initial data if provided
if (initialData.length > 0) {
const mapping = new Float32Array(buffer.getMappedRange());
mapping.set(initialData);
buffer.unmap();
}
const storageBuffer = new StorageBuffer(buffer, size, this);
// Track for cleanup
this._storageBuffers.add(storageBuffer);
return storageBuffer;
}
_getWebGPUColorFormat(framebuffer) {
if (framebuffer.format === FLOAT) {
return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float';
} else if (framebuffer.format === HALF_FLOAT) {
return framebuffer.channels === RGBA ? 'rgba16float' : 'rgba16float';
} else {
// Framebuffer with _useCanvasFormat should match canvas presentation format
if (framebuffer._useCanvasFormat) {
return this.presentationFormat;
}
// Other framebuffers use standard RGBA format
return framebuffer.channels === RGBA ? 'rgba8unorm' : 'rgba8unorm';
}
}
_getWebGPUDepthFormat(framebuffer) {
if (framebuffer._useCanvasFormat) {
return this.depthFormat;
}
if (framebuffer.useStencil) {
return framebuffer.depthFormat === FLOAT ? 'depth32float-stencil8' : 'depth24plus-stencil8';
} else {
return framebuffer.depthFormat === FLOAT ? 'depth32float' : 'depth24plus';
}
}
_deleteFramebufferTexture(texture) {
this.flushDraw();
const handle = texture.rawTexture();
if (handle.texture && handle.texture.destroy) {
const tex = handle.texture;
this._postSubmitCallbacks.push(() => tex.destroy());
}
this.textures.delete(texture);
}
deleteFramebufferTextures(framebuffer) {
this._deleteFramebufferTexture(framebuffer.color);
if (framebuffer.depth) this._deleteFramebufferTexture(framebuffer.depth);
}
deleteFramebufferResources(framebuffer) {
this.flushDraw();
if (framebuffer.colorTexture && framebuffer.colorTexture.destroy) {
const tex = framebuffer.colorTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
}
if (framebuffer.depthTexture && framebuffer.depthTexture.destroy) {
const tex = framebuffer.depthTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
}
if (framebuffer.aaDepthTexture && framebuffer.aaDepthTexture.destroy) {
const tex = framebuffer.aaDepthTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
}
}
getFramebufferToBind(framebuffer) {
}
updateFramebufferTexture(framebuffer, property) {
// No-op for WebGPU since antialiasing is handled at pipeline level
}
bindFramebuffer(framebuffer) {}
framebufferYScale() {
return 1;
}
async readFramebufferPixels(framebuffer) {
this.flushDraw();
// await this.finishDraw();
// Ensure all pending GPU work is complete before reading pixels
// await this.queue.onSubmittedWorkDone();
const width = framebuffer.width * framebuffer.density;
const height = framebuffer.height * framebuffer.density;
const bytesPerPixel = 4;
const unalignedBytesPerRow = width * bytesPerPixel;
const alignedBytesPerRow = this._alignBytesPerRow(unalignedBytesPerRow);
const bufferSize = alignedBytesPerRow * height;
// const stagingBuffer = this._ensurePixelReadBuffer(bufferSize);
const stagingBuffer = this.device.createBuffer({
size: bufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyTextureToBuffer(
{
texture: framebuffer.colorTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
aspect: 'all'
},
{ buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow, rowsPerImage: height },
{ width, height, depthOrArrayLayers: 1 }
);
this.device.queue.submit([commandEncoder.finish()]);
// Wait for the copy operation to complete
// await this.queue.onSubmittedWorkDone();
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize);
const mappedRange = stagingBuffer.getMappedRange(0, bufferSize);
// If alignment was needed, extract the actual pixel data
let result;
if (alignedBytesPerRow === unalignedBytesPerRow) {
result = new Uint8Array(mappedRange.slice(0, width * height * bytesPerPixel));
stagingBuffer.unmap();
} else {
// Need to extract pixel data from aligned buffer
result = new Uint8Array(width * height * bytesPerPixel);
const mappedData = new Uint8Array(mappedRange);
for (let y = 0; y < height; y++) {
const srcOffset = y * alignedBytesPerRow;
const dstOffset = y * unalignedBytesPerRow;
result.set(mappedData.subarray(srcOffset, srcOffset + unalignedBytesPerRow), dstOffset);
}
stagingBuffer.unmap();
}
this._ensurePixelsAreRGBA(framebuffer, result);
return result;
}
async readFramebufferPixel(framebuffer, x, y) {
this.flushDraw();
// await this.finishDraw();
// Ensure all pending GPU work is complete before reading pixels
// await this.queue.onSubmittedWorkDone();
const bytesPerPixel = 4;
const alignedBytesPerRow = this._alignBytesPerRow(bytesPerPixel);
const bufferSize = alignedBytesPerRow;
const stagingBuffer = this._ensurePixelReadBuffer(bufferSize);
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyTextureToBuffer(
{
texture: framebuffer.colorTexture,
origin: { x, y, z: 0 }
},
{ buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow },
{ width: 1, height: 1, depthOrArrayLayers: 1 }
);
this.device.queue.submit([commandEncoder.finish()]);
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize);
const mappedRange = stagingBuffer.getMappedRange(0, bufferSize);
const pixelData = new Uint8Array(mappedRange);
const result = [pixelData[0], pixelData[1], pixelData[2], pixelData[3]];
this._ensurePixelsAreRGBA(framebuffer, result);
stagingBuffer.unmap();
return result;
}
async readFramebufferRegion(framebuffer, x, y, w, h) {
this.flushDraw();
// await this.finishDraw();
// const wasActive = this.activeFramebuffer() === framebuffer;
// if (wasActive) {
// framebuffer.end();
// this.flushDraw()
// }
// Ensure all pending GPU work is complete before reading pixels
// await this.queue.onSubmittedWorkDone();
const width = w * framebuffer.density;
const height = h * framebuffer.density;
const bytesPerPixel = 4;
const unalignedBytesPerRow = width * bytesPerPixel;
const alignedBytesPerRow = this._alignBytesPerRow(unalignedBytesPerRow);
const bufferSize = alignedBytesPerRow * height;
const stagingBuffer = this._ensurePixelReadBuffer(bufferSize);
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyTextureToBuffer(
{
texture: framebuffer.colorTexture,
mipLevel: 0,
origin: { x: x * framebuffer.density, y: y * framebuffer.density, z: 0 }
},
{ buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow },
{ width, height, depthOrArrayLayers: 1 }
);
this.device.queue.submit([commandEncoder.finish()]);
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize);
const mappedRange = stagingBuffer.getMappedRange(0, bufferSize);
let pixelData;
if (alignedBytesPerRow === unalignedBytesPerRow) {
pixelData = new Uint8Array(mappedRange.slice(0, width * height * bytesPerPixel));
} else {
// Need to extract pixel data from aligned buffer
pixelData = new Uint8Array(width * height * bytesPerPixel);
const mappedData = new Uint8Array(mappedRange);
for (let y = 0; y < height; y++) {
const srcOffset = y * alignedBytesPerRow;
const dstOffset = y * unalignedBytesPerRow;
pixelData.set(mappedData.subarray(srcOffset, srcOffset + unalignedBytesPerRow), dstOffset);
}
}
this._ensurePixelsAreRGBA(framebuffer, pixelData);
// WebGPU doesn't need vertical flipping unlike WebGL
const region = new Image(width, height);
region.imageData = region.canvas.getContext('2d').createImageData(width, height);
region.imageData.data.set(pixelData);
region.pixels = region.imageData.data;
region.updatePixels();
if (framebuffer.density !== 1) {
region.pixelDensity(framebuffer.density);
}
stagingBuffer.unmap();
// if (wasActive) framebuffer.begin();
return region;
}
updateFramebufferPixels(framebuffer) {
const width = framebuffer.width * framebuffer.density;
const height = framebuffer.height * framebuffer.density;
const bytesPerPixel = 4;
const expectedLength = width * height * bytesPerPixel;
if (!framebuffer.pixels || framebuffer.pixels.length !== expectedLength) {
throw new Error(
'The pixels array has not been set correctly. Please call loadPixels() before updatePixels().'
);
}
this.device.queue.writeTexture(
{ texture: framebuffer.colorTexture },
framebuffer.pixels,
{
bytesPerRow: width * bytesPerPixel,
rowsPerImage: height
},
{ width, height, depthOrArrayLayers: 1 }
);
}
//////////////////////////////////////////////
// Main canvas pixel methods
//////////////////////////////////////////////
_ensurePixelsAreRGBA(framebuffer, result) {
// Convert BGRA to RGBA if reading from canvas-format framebuffer on BGRA systems
if (framebuffer._useCanvasFormat && this.presentationFormat === 'bgra8unorm') {
this._convertBGRtoRGB(result);
}
}
_convertBGRtoRGB(pixelData) {
for (let i = 0; i < pixelData.length; i += 4) {
const temp = pixelData[i];
pixelData[i] = pixelData[i + 2];
pixelData[i + 2] = temp;
}
}
async loadPixels() {
this._promoteToFramebuffer();
await this.mainFramebuffer.loadPixels();
this.pixels = this.mainFramebuffer.pixels.slice();
}
async get(x, y, w, h) {
this._promoteToFramebuffer();
return this.mainFramebuffer.get(x, y, w, h);
}
filter(...args) {
// If no custom framebuffer is active, promote to mainFramebuffer
if (!this.activeFramebuffer()) {
this._promoteToFramebuffer();
}
return super.filter(...args);
}
baseFilterShader() {
if (!this._baseFilterShader) {
this._baseFilterShader = new Shader(
this,
baseFilterVertexShader,
baseFilterFragmentShader,
{
vertex: {},
fragment: {
"vec4<f32> getColor": `(inputs: FilterInputs, canvasContent: texture_2d<f32>, canvasContent_sampler: sampler) -> vec4<f32> {
return textureSample(tex, tex_sampler, inputs.texCoord);
}`,
},
hookAliases: {
'getColor': ['filterColor'],
},
}
);
}
return this._baseFilterShader;
}
baseComputeShader() {
if (!this._baseComputeShader) {
this._baseComputeShader = new Shader(
this,
baseComputeShader,
{
compute: {
'void iteration': '(index: vec3<i32>) {}',
},
}
);
}
return this._baseComputeShader;
}
/*
* WebGPU-specific implementation of imageLight shader creation
*/
_createImageLightShader(type) {
if (type === 'diffused') {
return this._pInst.createShader(
imageLightVertexShader,
imageLightDiffusedFragmentShader
);
} else if (type === 'specular') {
return this._pInst.createShader(
imageLightVertexShader,
imageLightSpecularFragmentShader
);
}
throw new Error(`Unknown imageLight shader type: ${type}`);
}
/*
* WebGPU-specific implementation of mipmap texture creation
*/
_createMipmapTexture(levels) {
return new MipmapTexture(this, levels, {});
}
/*
* Prepare WebGPU texture to accumulate mip levels directly
*/
_prepareMipmapData(size, mipLevels) {
// Create WebGPU texture with mipmaps upfront
const textureDescriptor = {
size: {
width: size,
height: size,
depthOrArrayLayers: 1,
},
mipLevelCount: mipLevels,
format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
};
const gpuTexture = this.device.createTexture(textureDescriptor);
return {
gpuTexture,
size,
mipLevels,
format: 'rgba8unorm'
};
}
/*
* Copy framebuffer content directly to WebGPU texture mip level
*/
_accumulateMipLevel(framebuffer, mipmapData, mipLevel, width, height) {
this.flushDraw();
// Copy from framebuffer texture to the mip level
const commandEncoder = this.device.createCommandEncoder();
// Get the underlying WebGPU texture from the framebuffer
const sourceTexture = framebuffer.color.rawTexture().texture;
commandEncoder.copyTextureToTexture(
{
texture: sourceTexture,
origin: { x: 0, y: 0, z: 0 },
},
{
texture: mipmapData.gpuTexture,
mipLevel: mipLevel,
origin: { x: 0, y: 0, z: 0 },
},
{
width: width,
height: height,
depthOrArrayLayers: 1,
}
);
this.device.queue.submit([commandEncoder.finish()]);
}
/*
* Create final MipmapTexture from WebGPU texture
*/
_finalizeMipmapTexture(mipmapData) {
// Create a MipmapTexture that wraps the pre-built WebGPU texture
return new MipmapTexture(this, mipmapData, {});
}
createMipmapTextureHandle({ gpuTexture, format, dataType, width, height }) {
// WebGPU always uses pre-built GPU textures for mipmaps
return {
texture: gpuTexture,
view: gpuTexture.createView(),
glFormat: format || 'rgba8unorm',
glDataType: dataType || 'uint8'
};
}
compute(shader, x, y = 1, z = 1) {
if (shader.shaderType !== 'compute') {
throw new Error('compute() can only be called with a compute shader');
}
this._finishActiveRenderPass();
// Ensure shader is initialized and finalized
if (!shader._compiled) {
shader.init();
}
// Set default uniforms
shader.setDefaultUniforms();
shader.setUniform('uTotalCount', [x, y, z]);
// Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup)
const WORKGROUP_SIZE_X = 8;
const WORKGROUP_SIZE_Y = 8;
const WORKGROUP_SIZE_Z = 1;
// auto spreading: if any dimension is too large or for performance optimization,
// spread total iteration count across dimensions
const totalIterations = x * y * z;
const MAX_THREADS_PER_DIM = 65535 * 8;
let px = x;
let py = y;
let pz = z;
// we spread if we exceed GPU limits OR if it involves a large 1D dispatch
const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM;
const isLarge1D = totalIterations > 1024 && y === 1 && z === 1;
if (exceedsLimits || isLarge1D) {
// Always use 2D square spreading (√N × √N).
// Benchmarks showed 2D square equals or outperforms 3D cube at every
// scale tested, with simpler index reconstruction in the shader.
px = Math.ceil(Math.sqrt(totalIterations));
py = Math.ceil(totalIterations / px);
pz = 1;
}
shader.setUniform('uPhysicalCount', [px, py, pz]);
const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X);
const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y);
const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z);
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
this.setupShaderBindGroups(shader, passEncoder, {
compute: true,
workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z],
});
// Dispatch compute workgroups
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
passEncoder.end();
this.device.queue.submit([commandEncoder.finish()]);
}
}
p5.RendererWebGPU = RendererWebGPU;
p5.renderers[WEBGPU] = p5.RendererWebGPU;
// TODO: move this and the duplicate in the WebGL renderer to another file
fn.setAttributes = async function (key, value) {
return this._renderer._setAttributes(key, value);
};
}
if (typeof p5 !== "undefined") {
rendererWebGPU(p5, p5.prototype);
}
function index(p5){
p5.registerAddon(rendererWebGPU);
}
export { index as default };