p5 2.2.3 → 2.3.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accessibility/color_namer.js +9 -11
- package/dist/accessibility/describe.js +0 -1
- package/dist/accessibility/gridOutput.js +0 -1
- package/dist/accessibility/index.js +9 -10
- package/dist/accessibility/outputs.js +0 -1
- package/dist/accessibility/textOutput.js +0 -1
- package/dist/app.js +11 -10
- package/dist/app.node.js +122 -0
- package/dist/color/color_conversion.js +9 -11
- package/dist/color/creating_reading.js +1 -1
- package/dist/color/index.js +2 -2
- package/dist/color/p5.Color.js +1 -1
- package/dist/color/setting.js +25 -12
- package/dist/{constants-BdTiYOQI.js → constants-0wkVUfqa.js} +2 -2
- package/dist/core/States.js +1 -1
- package/dist/core/constants.js +1 -1
- package/dist/core/environment.js +28 -29
- package/dist/core/filterShaders.js +1 -1
- package/dist/core/friendly_errors/fes_core.js +9 -8
- package/dist/core/friendly_errors/file_errors.js +1 -2
- package/dist/core/friendly_errors/index.js +1 -1
- package/dist/core/friendly_errors/param_validator.js +737 -640
- package/dist/core/friendly_errors/sketch_verifier.js +1 -1
- package/dist/core/friendly_errors/stacktrace.js +0 -1
- package/dist/core/helpers.js +3 -4
- package/dist/core/init.js +24 -21
- package/dist/core/internationalization.js +1 -1
- package/dist/core/legacy.js +9 -11
- package/dist/core/main.js +9 -10
- package/dist/core/p5.Graphics.js +5 -5
- package/dist/core/p5.Renderer.js +3 -3
- package/dist/core/p5.Renderer2D.js +9 -10
- package/dist/core/p5.Renderer3D.js +5 -5
- package/dist/core/rendering.js +5 -5
- package/dist/core/structure.js +0 -1
- package/dist/core/transform.js +7 -16
- package/dist/{creating_reading-C7hu6sg1.js → creating_reading-DLkHH80h.js} +11 -8
- package/dist/data/local_storage.js +0 -1
- package/dist/dom/dom.js +2 -3
- package/dist/dom/index.js +2 -2
- package/dist/dom/p5.Element.js +2 -2
- package/dist/dom/p5.MediaElement.js +2 -2
- package/dist/events/acceleration.js +5 -3
- package/dist/events/keyboard.js +0 -1
- package/dist/events/pointer.js +0 -2
- package/dist/image/const.js +1 -1
- package/dist/image/filterRenderer2D.js +19 -12
- package/dist/image/image.js +5 -5
- package/dist/image/index.js +5 -5
- package/dist/image/loading_displaying.js +5 -5
- package/dist/image/p5.Image.js +3 -3
- package/dist/image/pixels.js +0 -1
- package/dist/io/files.js +5 -5
- package/dist/io/index.js +5 -5
- package/dist/io/p5.Table.js +0 -1
- package/dist/io/p5.TableRow.js +0 -1
- package/dist/io/p5.XML.js +0 -1
- package/dist/{ir_builders-Cd6rU9Vm.js → ir_builders-C2ebb6Lu.js} +234 -1
- package/dist/{main-H_nu4eDs.js → main-D2kqeMXM.js} +107 -136
- package/dist/math/Matrices/Matrix.js +1 -1
- package/dist/math/Matrices/MatrixNumjs.js +1 -1
- package/dist/math/calculation.js +0 -1
- package/dist/math/index.js +3 -1
- package/dist/math/math.js +3 -17
- package/dist/math/noise.js +0 -1
- package/dist/math/p5.Matrix.js +1 -2
- package/dist/math/p5.Vector.js +237 -279
- package/dist/math/patch-vector.js +75 -0
- package/dist/math/random.js +0 -1
- package/dist/math/trigonometry.js +3 -4
- package/dist/{p5.Renderer-BmD2P6Wv.js → p5.Renderer-CQI8PO1F.js} +31 -24
- package/dist/{rendering-CC8JNTwG.js → rendering-ltTIxpF2.js} +732 -44
- package/dist/shape/2d_primitives.js +1 -4
- package/dist/shape/attributes.js +43 -8
- package/dist/shape/curves.js +0 -1
- package/dist/shape/custom_shapes.js +260 -5
- package/dist/shape/index.js +2 -2
- package/dist/shape/vertex.js +0 -2
- package/dist/strands/ir_builders.js +1 -1
- package/dist/strands/ir_types.js +5 -1
- package/dist/strands/p5.strands.js +286 -31
- package/dist/strands/strands_api.js +179 -8
- package/dist/strands/strands_codegen.js +26 -8
- package/dist/strands/strands_conditionals.js +1 -1
- package/dist/strands/strands_for.js +1 -1
- package/dist/strands/strands_node.js +1 -1
- package/dist/strands/strands_ternary.js +56 -0
- package/dist/strands/strands_transpiler.js +416 -251
- package/dist/strands_glslBackend-i-ReKgZo.js +423 -0
- package/dist/type/index.js +3 -3
- package/dist/type/lib/Typr.js +1 -1
- package/dist/type/p5.Font.js +3 -3
- package/dist/type/textCore.js +31 -24
- package/dist/utilities/conversion.js +0 -1
- package/dist/utilities/time_date.js +0 -1
- package/dist/utilities/utility_functions.js +0 -1
- package/dist/webgl/3d_primitives.js +5 -5
- package/dist/webgl/GeometryBuilder.js +1 -1
- package/dist/webgl/ShapeBuilder.js +26 -1
- package/dist/webgl/enums.js +1 -1
- package/dist/webgl/index.js +8 -9
- package/dist/webgl/interaction.js +8 -4
- package/dist/webgl/light.js +5 -5
- package/dist/webgl/loading.js +60 -21
- package/dist/webgl/material.js +5 -5
- package/dist/webgl/p5.Camera.js +5 -5
- package/dist/webgl/p5.Framebuffer.js +5 -5
- package/dist/webgl/p5.Geometry.js +3 -5
- package/dist/webgl/p5.Quat.js +1 -1
- package/dist/webgl/p5.RendererGL.js +17 -21
- package/dist/webgl/p5.Shader.js +129 -36
- package/dist/webgl/p5.Texture.js +5 -5
- package/dist/webgl/strands_glslBackend.js +5 -386
- package/dist/webgl/text.js +5 -5
- package/dist/webgl/utils.js +5 -5
- package/dist/webgl2Compatibility-DA7DLMuq.js +7 -0
- package/dist/webgpu/index.js +7 -3
- package/dist/webgpu/p5.RendererWebGPU.js +1146 -180
- package/dist/webgpu/shaders/color.js +1 -1
- package/dist/webgpu/shaders/compute.js +32 -0
- package/dist/webgpu/shaders/functions/randomComputeWGSL.js +31 -0
- package/dist/webgpu/shaders/functions/randomVertWGSL.js +30 -0
- package/dist/webgpu/shaders/functions/randomWGSL.js +30 -0
- package/dist/webgpu/shaders/line.js +1 -1
- package/dist/webgpu/shaders/material.js +3 -3
- package/dist/webgpu/strands_wgslBackend.js +137 -15
- package/lib/p5.esm.js +4092 -1950
- package/lib/p5.esm.min.js +1 -1
- package/lib/p5.js +4092 -1950
- package/lib/p5.min.js +1 -1
- package/lib/p5.webgpu.esm.js +1748 -306
- package/lib/p5.webgpu.js +1747 -305
- package/lib/p5.webgpu.min.js +1 -1
- package/package.json +6 -1
- package/types/global.d.ts +4182 -2441
- package/types/p5.d.ts +2776 -1675
- package/dist/noise3DGLSL-Bwrdi4gi.js +0 -9
package/lib/p5.webgpu.esm.js
CHANGED
|
@@ -11,7 +11,7 @@ const _PI = Math.PI;
|
|
|
11
11
|
* @property {String} VERSION
|
|
12
12
|
* @final
|
|
13
13
|
*/
|
|
14
|
-
const VERSION = '2.
|
|
14
|
+
const VERSION = '2.3.0-rc.1';
|
|
15
15
|
|
|
16
16
|
// GRAPHICS RENDERER
|
|
17
17
|
/**
|
|
@@ -1433,6 +1433,7 @@ const NodeType = {
|
|
|
1433
1433
|
STATEMENT: 'statement',
|
|
1434
1434
|
ASSIGNMENT: 'assignment',
|
|
1435
1435
|
};
|
|
1436
|
+
const INSTANCE_ID_VARYING_NAME = '_p5_instanceID';
|
|
1436
1437
|
const NodeTypeToName = Object.fromEntries(
|
|
1437
1438
|
Object.entries(NodeType).map(([key, val]) => [val, key])
|
|
1438
1439
|
);
|
|
@@ -1526,6 +1527,7 @@ const OpCode = {
|
|
|
1526
1527
|
LOGICAL_AND: 11,
|
|
1527
1528
|
LOGICAL_OR: 12,
|
|
1528
1529
|
MEMBER_ACCESS: 13,
|
|
1530
|
+
ARRAY_ACCESS: 14,
|
|
1529
1531
|
},
|
|
1530
1532
|
Unary: {
|
|
1531
1533
|
LOGICAL_NOT: 100,
|
|
@@ -1536,7 +1538,7 @@ const OpCode = {
|
|
|
1536
1538
|
Nary: {
|
|
1537
1539
|
FUNCTION_CALL: 200,
|
|
1538
1540
|
CONSTRUCTOR: 201,
|
|
1539
|
-
|
|
1541
|
+
TERNARY: 202}};
|
|
1540
1542
|
const OperatorTable = [
|
|
1541
1543
|
{ arity: "unary", boolean: true, name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT },
|
|
1542
1544
|
{ arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE },
|
|
@@ -1699,7 +1701,7 @@ ${uniforms$5}
|
|
|
1699
1701
|
@fragment
|
|
1700
1702
|
fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
1701
1703
|
HOOK_beforeFragment();
|
|
1702
|
-
var outColor = HOOK_getFinalColor(input.vColor);
|
|
1704
|
+
var outColor = HOOK_getFinalColor(input.vColor, input.vVertTexCoord);
|
|
1703
1705
|
outColor = vec4<f32>(outColor.rgb * outColor.a, outColor.a);
|
|
1704
1706
|
HOOK_afterFragment();
|
|
1705
1707
|
return outColor;
|
|
@@ -2070,7 +2072,7 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4<f32> {
|
|
|
2070
2072
|
discard;
|
|
2071
2073
|
}
|
|
2072
2074
|
}
|
|
2073
|
-
var col = HOOK_getFinalColor(inputs.color);
|
|
2075
|
+
var col = HOOK_getFinalColor(inputs.color, vec2<f32>(0.0, 0.0));
|
|
2074
2076
|
col = vec4<f32>(col.rgb, 1.0) * col.a;
|
|
2075
2077
|
HOOK_afterFragment();
|
|
2076
2078
|
return vec4<f32>(col);
|
|
@@ -2495,9 +2497,9 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
2495
2497
|
inputs.emissiveMaterial
|
|
2496
2498
|
);
|
|
2497
2499
|
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2500
|
+
var outColor = HOOK_getFinalColor(
|
|
2501
|
+
HOOK_combineColors(components), input.vTexCoord
|
|
2502
|
+
);
|
|
2501
2503
|
outColor = vec4<f32>(outColor.rgb * outColor.a, outColor.a);
|
|
2502
2504
|
HOOK_afterFragment();
|
|
2503
2505
|
return outColor;
|
|
@@ -2849,6 +2851,201 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
2849
2851
|
}
|
|
2850
2852
|
`;
|
|
2851
2853
|
|
|
2854
|
+
// Based on https://github.com/stegu/webgl-noise/blob/22434e04d7753f7e949e8d724ab3da2864c17a0f/src/noise3D.glsl
|
|
2855
|
+
// MIT licensed, adapted for p5.strands and converted to WGSL
|
|
2856
|
+
|
|
2857
|
+
var noiseWGSL = `fn mod289Vec3(x: vec3<f32>) -> vec3<f32> {
|
|
2858
|
+
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
fn mod289Vec4(x: vec4<f32>) -> vec4<f32> {
|
|
2862
|
+
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
fn permute(x: vec4<f32>) -> vec4<f32> {
|
|
2866
|
+
return mod289Vec4(((x*34.0)+10.0)*x);
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
fn taylorInvSqrt(r: vec4<f32>) -> vec4<f32> {
|
|
2870
|
+
return vec4<f32>(1.79284291400159) - vec4<f32>(0.85373472095314) * r;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
fn baseNoise(v: vec3<f32>) -> f32 {
|
|
2874
|
+
let C = vec2<f32>(1.0/6.0, 1.0/3.0);
|
|
2875
|
+
let D = vec4<f32>(0.0, 0.5, 1.0, 2.0);
|
|
2876
|
+
|
|
2877
|
+
// First corner
|
|
2878
|
+
var i = floor(v + dot(v, C.yyy));
|
|
2879
|
+
let x0 = v - i + dot(i, C.xxx);
|
|
2880
|
+
|
|
2881
|
+
// Other corners
|
|
2882
|
+
let g = step(x0.yzx, x0.xyz);
|
|
2883
|
+
let l = vec3<f32>(1.0) - g;
|
|
2884
|
+
let i1 = min(g.xyz, l.zxy);
|
|
2885
|
+
let i2 = max(g.xyz, l.zxy);
|
|
2886
|
+
|
|
2887
|
+
// x0 = x0 - 0.0 + 0.0 * C.xxx;
|
|
2888
|
+
// x1 = x0 - i1 + 1.0 * C.xxx;
|
|
2889
|
+
// x2 = x0 - i2 + 2.0 * C.xxx;
|
|
2890
|
+
// x3 = x0 - 1.0 + 3.0 * C.xxx;
|
|
2891
|
+
let x1 = x0 - i1 + C.xxx;
|
|
2892
|
+
let x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
|
|
2893
|
+
let x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
|
|
2894
|
+
|
|
2895
|
+
// Permutations
|
|
2896
|
+
i = mod289Vec3(i);
|
|
2897
|
+
let p = permute( permute( permute(
|
|
2898
|
+
i.z + vec4<f32>(0.0, i1.z, i2.z, 1.0 ))
|
|
2899
|
+
+ i.y + vec4<f32>(0.0, i1.y, i2.y, 1.0 ))
|
|
2900
|
+
+ i.x + vec4<f32>(0.0, i1.x, i2.x, 1.0 ));
|
|
2901
|
+
|
|
2902
|
+
// Gradients: 7x7 points over a square, mapped onto an octahedron.
|
|
2903
|
+
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
|
|
2904
|
+
let n_ = 0.142857142857; // 1.0/7.0
|
|
2905
|
+
let ns = n_ * D.wyz - D.xzx;
|
|
2906
|
+
|
|
2907
|
+
let j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
|
|
2908
|
+
|
|
2909
|
+
let x_ = floor(j * ns.z);
|
|
2910
|
+
let y_ = floor(j - 7.0 * x_ ); // mod(j,N)
|
|
2911
|
+
|
|
2912
|
+
let x = x_ *ns.x + ns.yyyy;
|
|
2913
|
+
let y = y_ *ns.x + ns.yyyy;
|
|
2914
|
+
let h = vec4<f32>(1.0) - abs(x) - abs(y);
|
|
2915
|
+
|
|
2916
|
+
let b0 = vec4<f32>( x.xy, y.xy );
|
|
2917
|
+
let b1 = vec4<f32>( x.zw, y.zw );
|
|
2918
|
+
|
|
2919
|
+
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
|
|
2920
|
+
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
|
|
2921
|
+
let s0 = floor(b0)*2.0 + vec4<f32>(1.0);
|
|
2922
|
+
let s1 = floor(b1)*2.0 + vec4<f32>(1.0);
|
|
2923
|
+
let sh = -step(h, vec4<f32>(0.0));
|
|
2924
|
+
|
|
2925
|
+
let a0 = b0.xzyw + s0.xzyw*sh.xxyy;
|
|
2926
|
+
let a1 = b1.xzyw + s1.xzyw*sh.zzww;
|
|
2927
|
+
|
|
2928
|
+
let p0 = vec3<f32>(a0.xy, h.x);
|
|
2929
|
+
let p1 = vec3<f32>(a0.zw, h.y);
|
|
2930
|
+
let p2 = vec3<f32>(a1.xy, h.z);
|
|
2931
|
+
let p3 = vec3<f32>(a1.zw, h.w);
|
|
2932
|
+
|
|
2933
|
+
//Normalise gradients
|
|
2934
|
+
let norm = taylorInvSqrt(vec4<f32>(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
|
2935
|
+
let p0_norm = p0 * norm.x;
|
|
2936
|
+
let p1_norm = p1 * norm.y;
|
|
2937
|
+
let p2_norm = p2 * norm.z;
|
|
2938
|
+
let p3_norm = p3 * norm.w;
|
|
2939
|
+
|
|
2940
|
+
// Mix final noise value
|
|
2941
|
+
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));
|
|
2942
|
+
m = m * m;
|
|
2943
|
+
return 105.0 * dot( m*m, vec4<f32>( dot(p0_norm,x0), dot(p1_norm,x1),
|
|
2944
|
+
dot(p2_norm,x2), dot(p3_norm,x3) ) );
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
fn noise(st: vec3<f32>, octaves: i32, ampFalloff: f32) -> f32 {
|
|
2948
|
+
var result = 0.0;
|
|
2949
|
+
var amplitude = 1.0;
|
|
2950
|
+
var frequency = 1.0;
|
|
2951
|
+
|
|
2952
|
+
for (var i = 0; i < 8; i++) {
|
|
2953
|
+
if (i >= octaves) { break; }
|
|
2954
|
+
result += amplitude * baseNoise(st * frequency);
|
|
2955
|
+
frequency *= 2.0;
|
|
2956
|
+
amplitude *= ampFalloff;
|
|
2957
|
+
}
|
|
2958
|
+
return (result + 1.0) * 0.5;
|
|
2959
|
+
}`;
|
|
2960
|
+
|
|
2961
|
+
// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
|
|
2962
|
+
// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
|
|
2963
|
+
// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
|
|
2964
|
+
// α₂ = 1/φ₂² = 0.5698402910
|
|
2965
|
+
// 1/φ = 0.6180339887 (golden ratio conjugate)
|
|
2966
|
+
//
|
|
2967
|
+
// Fragment shader version: pixelCoord is passed in from main via @builtin(position).
|
|
2968
|
+
|
|
2969
|
+
var randomWGSL = `
|
|
2970
|
+
var<private> _p5_randomCallIndex: i32 = 0;
|
|
2971
|
+
|
|
2972
|
+
fn _p5_hash(p: vec3<f32>) -> f32 {
|
|
2973
|
+
var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
|
|
2974
|
+
p3 = p3 + dot(p3, p3.yxz + 33.33);
|
|
2975
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
fn random(seed: f32, pixelCoord: vec2<f32>) -> f32 {
|
|
2979
|
+
let callIndex = f32(_p5_randomCallIndex);
|
|
2980
|
+
_p5_randomCallIndex = _p5_randomCallIndex + 1;
|
|
2981
|
+
let s = fract(seed * 0.7548776662);
|
|
2982
|
+
return _p5_hash(vec3<f32>(
|
|
2983
|
+
pixelCoord.x + s,
|
|
2984
|
+
pixelCoord.y + callIndex * 0.5698402910,
|
|
2985
|
+
s + callIndex * 0.6180339887
|
|
2986
|
+
));
|
|
2987
|
+
}
|
|
2988
|
+
`;
|
|
2989
|
+
|
|
2990
|
+
// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
|
|
2991
|
+
// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
|
|
2992
|
+
// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
|
|
2993
|
+
// α₂ = 1/φ₂² = 0.5698402910
|
|
2994
|
+
// 1/φ = 0.6180339887 (golden ratio conjugate)
|
|
2995
|
+
//
|
|
2996
|
+
// Vertex shader version: vertexId is passed in from main via @builtin(vertex_index).
|
|
2997
|
+
|
|
2998
|
+
var randomVertWGSL = `
|
|
2999
|
+
var<private> _p5_randomCallIndex: i32 = 0;
|
|
3000
|
+
|
|
3001
|
+
fn _p5_hash(p: vec3<f32>) -> f32 {
|
|
3002
|
+
var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
|
|
3003
|
+
p3 = p3 + dot(p3, p3.yxz + 33.33);
|
|
3004
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
fn random(seed: f32, vertexId: f32) -> f32 {
|
|
3008
|
+
let callIndex = f32(_p5_randomCallIndex);
|
|
3009
|
+
_p5_randomCallIndex = _p5_randomCallIndex + 1;
|
|
3010
|
+
let s = fract(seed * 0.7548776662);
|
|
3011
|
+
return _p5_hash(vec3<f32>(
|
|
3012
|
+
vertexId + s,
|
|
3013
|
+
vertexId * 0.5698402910 + callIndex * 0.6180339887,
|
|
3014
|
+
s + callIndex * 0.7548776662
|
|
3015
|
+
));
|
|
3016
|
+
}
|
|
3017
|
+
`;
|
|
3018
|
+
|
|
3019
|
+
// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
|
|
3020
|
+
// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
|
|
3021
|
+
// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
|
|
3022
|
+
// α₂ = 1/φ₂² = 0.5698402910
|
|
3023
|
+
// 1/φ = 0.6180339887 (golden ratio conjugate)
|
|
3024
|
+
//
|
|
3025
|
+
// Compute shader version: invocationId is passed in from main via @builtin(global_invocation_id).
|
|
3026
|
+
|
|
3027
|
+
var randomComputeWGSL = `
|
|
3028
|
+
var<private> _p5_randomCallIndex: i32 = 0;
|
|
3029
|
+
|
|
3030
|
+
fn _p5_hash(p: vec3<f32>) -> f32 {
|
|
3031
|
+
var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
|
|
3032
|
+
p3 = p3 + dot(p3, p3.yxz + 33.33);
|
|
3033
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
fn random(seed: f32, invocationId: vec3<u32>) -> f32 {
|
|
3037
|
+
let id = vec3<f32>(invocationId);
|
|
3038
|
+
let callIndex = f32(_p5_randomCallIndex);
|
|
3039
|
+
_p5_randomCallIndex = _p5_randomCallIndex + 1;
|
|
3040
|
+
let s = fract(seed * 0.7548776662);
|
|
3041
|
+
return _p5_hash(vec3<f32>(
|
|
3042
|
+
id.x + s,
|
|
3043
|
+
id.y + callIndex * 0.5698402910,
|
|
3044
|
+
id.z + s + callIndex * 0.6180339887
|
|
3045
|
+
));
|
|
3046
|
+
}
|
|
3047
|
+
`;
|
|
3048
|
+
|
|
2852
3049
|
function internalError(errorMessage) {
|
|
2853
3050
|
const prefixedMessage = `[p5.strands internal error]: ${errorMessage}`;
|
|
2854
3051
|
throw new Error(prefixedMessage);
|
|
@@ -2978,6 +3175,9 @@ class StrandsNode {
|
|
|
2978
3175
|
this.strandsContext = strandsContext;
|
|
2979
3176
|
this.dimension = dimension;
|
|
2980
3177
|
this.structProperties = null;
|
|
3178
|
+
// Schema for struct storage buffers (set by uniformStorage when buffer has a struct layout).
|
|
3179
|
+
// When set, buf.get(idx) returns a field proxy instead of a scalar StrandsNode.
|
|
3180
|
+
this._schema = null;
|
|
2981
3181
|
this.isStrandsNode = true;
|
|
2982
3182
|
|
|
2983
3183
|
// Store original identifier for varying variables
|
|
@@ -3131,11 +3331,61 @@ class StrandsNode {
|
|
|
3131
3331
|
|
|
3132
3332
|
return this;
|
|
3133
3333
|
}
|
|
3334
|
+
|
|
3335
|
+
get(index) {
|
|
3336
|
+
// Validate baseType is 'storage'
|
|
3337
|
+
const nodeData = getNodeDataFromID(this.strandsContext.dag, this.id);
|
|
3338
|
+
if (nodeData.baseType !== 'storage') {
|
|
3339
|
+
throw new Error('get() can only be used on storage buffers');
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
// For struct storage, return a proxy with per-field getters/setters
|
|
3343
|
+
if (this._schema) {
|
|
3344
|
+
return createStructArrayElementProxy(this.strandsContext, this, index, this._schema);
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// Create array access node: buffer.get(index) -> buffer[index]
|
|
3348
|
+
const { id, dimension } = arrayAccessNode(
|
|
3349
|
+
this.strandsContext,
|
|
3350
|
+
this,
|
|
3351
|
+
index);
|
|
3352
|
+
return createStrandsNode(id, dimension, this.strandsContext);
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
set(index, value) {
|
|
3356
|
+
// Validate baseType is 'storage' and has _originalIdentifier
|
|
3357
|
+
const nodeData = getNodeDataFromID(this.strandsContext.dag, this.id);
|
|
3358
|
+
if (nodeData.baseType !== 'storage') {
|
|
3359
|
+
throw new Error('set() can only be used on storage buffers');
|
|
3360
|
+
}
|
|
3361
|
+
if (!this._originalIdentifier) {
|
|
3362
|
+
throw new Error('set() can only be used on storage buffers with an identifier');
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// If value is a plain object (struct literal), expand to per-field assignments
|
|
3366
|
+
// e.g. buf[idx] = { position: pos, velocity: vel }
|
|
3367
|
+
// becomes buf[idx].position = pos; buf[idx].velocity = vel;
|
|
3368
|
+
if (value !== null && typeof value === 'object' && !value.isStrandsNode && this._schema) {
|
|
3369
|
+
const proxy = createStructArrayElementProxy(this.strandsContext, this, index, this._schema);
|
|
3370
|
+
for (const [fieldName, fieldValue] of Object.entries(value)) {
|
|
3371
|
+
proxy[fieldName] = fieldValue;
|
|
3372
|
+
}
|
|
3373
|
+
return this;
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// Create array assignment node: buffer.set(index, value) -> buffer[index] = value
|
|
3377
|
+
// This creates an ASSIGNMENT node and records it in the CFG basic block
|
|
3378
|
+
// CFG preserves sequential order, preventing reordering of assignments
|
|
3379
|
+
arrayAssignmentNode(this.strandsContext, this, index, value);
|
|
3380
|
+
|
|
3381
|
+
// Return this for chaining
|
|
3382
|
+
return this;
|
|
3383
|
+
}
|
|
3134
3384
|
}
|
|
3135
3385
|
function createStrandsNode(id, dimension, strandsContext, onRebind) {
|
|
3136
3386
|
return new Proxy(
|
|
3137
3387
|
new StrandsNode(id, dimension, strandsContext),
|
|
3138
|
-
swizzleTrap(id, dimension, strandsContext)
|
|
3388
|
+
swizzleTrap(id, dimension, strandsContext, onRebind)
|
|
3139
3389
|
);
|
|
3140
3390
|
}
|
|
3141
3391
|
|
|
@@ -3625,6 +3875,14 @@ function swizzleTrap(id, dimension, strandsContext, onRebind) {
|
|
|
3625
3875
|
);
|
|
3626
3876
|
|
|
3627
3877
|
target.id = newID;
|
|
3878
|
+
|
|
3879
|
+
// If we swizzle assign on a struct component i.e.
|
|
3880
|
+
// inputs.position.rg = [1, 2]
|
|
3881
|
+
// The onRebind callback will update the structs components so that it refers to the new values,
|
|
3882
|
+
// and make a new ID for the struct with these new values
|
|
3883
|
+
if (typeof onRebind === 'function') {
|
|
3884
|
+
onRebind(newID);
|
|
3885
|
+
}
|
|
3628
3886
|
return true;
|
|
3629
3887
|
}
|
|
3630
3888
|
return Reflect.set(...arguments);
|
|
@@ -3633,6 +3891,186 @@ function swizzleTrap(id, dimension, strandsContext, onRebind) {
|
|
|
3633
3891
|
return trap;
|
|
3634
3892
|
}
|
|
3635
3893
|
|
|
3894
|
+
function arrayAccessNode(strandsContext, bufferNode, indexNode, accessMode) {
|
|
3895
|
+
const { dag, cfg } = strandsContext;
|
|
3896
|
+
|
|
3897
|
+
// Ensure index is a StrandsNode
|
|
3898
|
+
let index;
|
|
3899
|
+
if (indexNode instanceof StrandsNode) {
|
|
3900
|
+
index = indexNode;
|
|
3901
|
+
} else {
|
|
3902
|
+
const { id, dimension } = primitiveConstructorNode(
|
|
3903
|
+
strandsContext,
|
|
3904
|
+
{ baseType: BaseType.INT, dimension: 1 },
|
|
3905
|
+
indexNode
|
|
3906
|
+
);
|
|
3907
|
+
index = createStrandsNode(id, dimension, strandsContext);
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
// Array access returns a single float
|
|
3911
|
+
const nodeData = createNodeData({
|
|
3912
|
+
nodeType: NodeType.OPERATION,
|
|
3913
|
+
opCode: OpCode.Binary.ARRAY_ACCESS,
|
|
3914
|
+
dependsOn: [bufferNode.id, index.id],
|
|
3915
|
+
dimension: 1,
|
|
3916
|
+
baseType: BaseType.FLOAT});
|
|
3917
|
+
|
|
3918
|
+
const id = getOrCreateNode(dag, nodeData);
|
|
3919
|
+
recordInBasicBlock(cfg, cfg.currentBlock, id);
|
|
3920
|
+
|
|
3921
|
+
return { id, dimension: 1 };
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3924
|
+
function createStructArrayElementProxy(strandsContext, bufferNode, indexNode, schema) {
|
|
3925
|
+
const { dag, cfg } = strandsContext;
|
|
3926
|
+
|
|
3927
|
+
// Ensure index is a StrandsNode
|
|
3928
|
+
let index;
|
|
3929
|
+
if (indexNode instanceof StrandsNode) {
|
|
3930
|
+
index = indexNode;
|
|
3931
|
+
} else {
|
|
3932
|
+
const { id, dimension } = primitiveConstructorNode(
|
|
3933
|
+
strandsContext,
|
|
3934
|
+
{ baseType: BaseType.INT, dimension: 1 },
|
|
3935
|
+
indexNode
|
|
3936
|
+
);
|
|
3937
|
+
index = createStrandsNode(id, dimension, strandsContext);
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
// Create a plain object with getters/setters for each struct field.
|
|
3941
|
+
// When read, a field creates an ARRAY_ACCESS IR node with the field name encoded
|
|
3942
|
+
// in the identifier slot. When written, an ASSIGNMENT IR node is recorded in the CFG.
|
|
3943
|
+
const proxy = {};
|
|
3944
|
+
|
|
3945
|
+
for (const field of schema.fields) {
|
|
3946
|
+
Object.defineProperty(proxy, field.name, {
|
|
3947
|
+
get() {
|
|
3948
|
+
// Encode field name in identifier so WGSL backend can emit buf[idx].field
|
|
3949
|
+
const nodeData = createNodeData({
|
|
3950
|
+
nodeType: NodeType.OPERATION,
|
|
3951
|
+
opCode: OpCode.Binary.ARRAY_ACCESS,
|
|
3952
|
+
dependsOn: [bufferNode.id, index.id],
|
|
3953
|
+
dimension: field.dim,
|
|
3954
|
+
baseType: BaseType.FLOAT,
|
|
3955
|
+
identifier: field.name,
|
|
3956
|
+
});
|
|
3957
|
+
const id = getOrCreateNode(dag, nodeData);
|
|
3958
|
+
recordInBasicBlock(cfg, cfg.currentBlock, id);
|
|
3959
|
+
// When a swizzle assignment fires (e.g. buf[i].vel.y *= -1), onRebind
|
|
3960
|
+
// receives the new vector ID and writes it back to the buffer field,
|
|
3961
|
+
// equivalent to buf[i].vel = newVec.
|
|
3962
|
+
const onRebind = (newFieldID) => {
|
|
3963
|
+
const accessData = createNodeData({
|
|
3964
|
+
nodeType: NodeType.OPERATION,
|
|
3965
|
+
opCode: OpCode.Binary.ARRAY_ACCESS,
|
|
3966
|
+
dependsOn: [bufferNode.id, index.id],
|
|
3967
|
+
dimension: field.dim,
|
|
3968
|
+
baseType: BaseType.FLOAT,
|
|
3969
|
+
identifier: field.name,
|
|
3970
|
+
});
|
|
3971
|
+
const accessID = getOrCreateNode(dag, accessData);
|
|
3972
|
+
const assignData = createNodeData({
|
|
3973
|
+
nodeType: NodeType.ASSIGNMENT,
|
|
3974
|
+
dependsOn: [accessID, newFieldID],
|
|
3975
|
+
phiBlocks: [],
|
|
3976
|
+
});
|
|
3977
|
+
const assignID = getOrCreateNode(dag, assignData);
|
|
3978
|
+
recordInBasicBlock(cfg, cfg.currentBlock, assignID);
|
|
3979
|
+
};
|
|
3980
|
+
return createStrandsNode(id, field.dim, strandsContext, onRebind);
|
|
3981
|
+
},
|
|
3982
|
+
set(val) {
|
|
3983
|
+
// Create access node as assignment target (field name in identifier)
|
|
3984
|
+
const accessData = createNodeData({
|
|
3985
|
+
nodeType: NodeType.OPERATION,
|
|
3986
|
+
opCode: OpCode.Binary.ARRAY_ACCESS,
|
|
3987
|
+
dependsOn: [bufferNode.id, index.id],
|
|
3988
|
+
dimension: field.dim,
|
|
3989
|
+
baseType: BaseType.FLOAT,
|
|
3990
|
+
identifier: field.name,
|
|
3991
|
+
});
|
|
3992
|
+
const accessID = getOrCreateNode(dag, accessData);
|
|
3993
|
+
|
|
3994
|
+
let valueID;
|
|
3995
|
+
if (val?.isStrandsNode) {
|
|
3996
|
+
valueID = val.id;
|
|
3997
|
+
} else {
|
|
3998
|
+
const { id } = primitiveConstructorNode(
|
|
3999
|
+
strandsContext,
|
|
4000
|
+
{ baseType: BaseType.FLOAT, dimension: field.dim },
|
|
4001
|
+
val
|
|
4002
|
+
);
|
|
4003
|
+
valueID = id;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
const assignData = createNodeData({
|
|
4007
|
+
nodeType: NodeType.ASSIGNMENT,
|
|
4008
|
+
dependsOn: [accessID, valueID],
|
|
4009
|
+
phiBlocks: [],
|
|
4010
|
+
});
|
|
4011
|
+
const assignID = getOrCreateNode(dag, assignData);
|
|
4012
|
+
recordInBasicBlock(cfg, cfg.currentBlock, assignID);
|
|
4013
|
+
},
|
|
4014
|
+
configurable: true,
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
return proxy;
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
function arrayAssignmentNode(strandsContext, bufferNode, indexNode, valueNode) {
|
|
4022
|
+
const { dag, cfg } = strandsContext;
|
|
4023
|
+
|
|
4024
|
+
// Ensure index is a StrandsNode
|
|
4025
|
+
let index;
|
|
4026
|
+
if (indexNode instanceof StrandsNode) {
|
|
4027
|
+
index = indexNode;
|
|
4028
|
+
} else {
|
|
4029
|
+
const { id, dimension } = primitiveConstructorNode(
|
|
4030
|
+
strandsContext,
|
|
4031
|
+
{ baseType: BaseType.INT, dimension: 1 },
|
|
4032
|
+
indexNode
|
|
4033
|
+
);
|
|
4034
|
+
index = createStrandsNode(id, dimension, strandsContext);
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
// Ensure value is a StrandsNode
|
|
4038
|
+
let value;
|
|
4039
|
+
if (valueNode instanceof StrandsNode) {
|
|
4040
|
+
value = valueNode;
|
|
4041
|
+
} else {
|
|
4042
|
+
const { id, dimension } = primitiveConstructorNode(
|
|
4043
|
+
strandsContext,
|
|
4044
|
+
{ baseType: BaseType.FLOAT, dimension: 1 },
|
|
4045
|
+
valueNode
|
|
4046
|
+
);
|
|
4047
|
+
value = createStrandsNode(id, dimension, strandsContext);
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
// Create array access node as the assignment target
|
|
4051
|
+
const arrayAccessData = createNodeData({
|
|
4052
|
+
nodeType: NodeType.OPERATION,
|
|
4053
|
+
opCode: OpCode.Binary.ARRAY_ACCESS,
|
|
4054
|
+
dependsOn: [bufferNode.id, index.id],
|
|
4055
|
+
dimension: 1,
|
|
4056
|
+
baseType: BaseType.FLOAT
|
|
4057
|
+
});
|
|
4058
|
+
const arrayAccessID = getOrCreateNode(dag, arrayAccessData);
|
|
4059
|
+
|
|
4060
|
+
// Create assignment node: buffer[index] = value
|
|
4061
|
+
const assignmentData = createNodeData({
|
|
4062
|
+
nodeType: NodeType.ASSIGNMENT,
|
|
4063
|
+
dependsOn: [arrayAccessID, value.id],
|
|
4064
|
+
phiBlocks: []
|
|
4065
|
+
});
|
|
4066
|
+
const assignmentID = getOrCreateNode(dag, assignmentData);
|
|
4067
|
+
|
|
4068
|
+
// CRITICAL: Record in CFG to preserve sequential ordering
|
|
4069
|
+
recordInBasicBlock(cfg, cfg.currentBlock, assignmentID);
|
|
4070
|
+
|
|
4071
|
+
return { id: assignmentID };
|
|
4072
|
+
}
|
|
4073
|
+
|
|
3636
4074
|
function shouldCreateTemp(dag, nodeID) {
|
|
3637
4075
|
const nodeType = dag.nodeTypes[nodeID];
|
|
3638
4076
|
if (nodeType !== NodeType.OPERATION) return false;
|
|
@@ -3834,10 +4272,10 @@ const wgslBackend = {
|
|
|
3834
4272
|
// Add texture and sampler bindings for sampler2D uniforms to both vertex and fragment declarations
|
|
3835
4273
|
if (!strandsContext.renderer || !strandsContext.baseShader) return;
|
|
3836
4274
|
|
|
3837
|
-
// Get the next available binding index from the renderer
|
|
3838
4275
|
let bindingIndex = strandsContext.renderer.getNextBindingIndex({
|
|
3839
|
-
vert: strandsContext.baseShader.
|
|
3840
|
-
frag: strandsContext.baseShader.
|
|
4276
|
+
vert: strandsContext.baseShader._vertSrc,
|
|
4277
|
+
frag: strandsContext.baseShader._fragSrc,
|
|
4278
|
+
compute: strandsContext.baseShader._computeSrc,
|
|
3841
4279
|
});
|
|
3842
4280
|
|
|
3843
4281
|
for (const {name, typeInfo} of strandsContext.uniforms) {
|
|
@@ -3854,6 +4292,38 @@ const wgslBackend = {
|
|
|
3854
4292
|
}
|
|
3855
4293
|
}
|
|
3856
4294
|
},
|
|
4295
|
+
addStorageBufferBindingsToDeclarations(strandsContext) {
|
|
4296
|
+
if (!strandsContext.renderer || !strandsContext.baseShader) return;
|
|
4297
|
+
|
|
4298
|
+
const isComputeShader = strandsContext.baseShader.shaderType === 'compute';
|
|
4299
|
+
let bindingIndex = strandsContext.renderer.getNextBindingIndex({
|
|
4300
|
+
vert: strandsContext.baseShader._vertSrc,
|
|
4301
|
+
frag: strandsContext.baseShader._fragSrc,
|
|
4302
|
+
compute: strandsContext.baseShader._computeSrc,
|
|
4303
|
+
});
|
|
4304
|
+
|
|
4305
|
+
for (const {name, typeInfo} of strandsContext.uniforms) {
|
|
4306
|
+
if (typeInfo.baseType === 'storage') {
|
|
4307
|
+
const accessMode = isComputeShader ? 'read_write' : 'read';
|
|
4308
|
+
let declaration;
|
|
4309
|
+
if (typeInfo.schema) {
|
|
4310
|
+
const structTypeName = `${name}Element`;
|
|
4311
|
+
declaration = `struct ${structTypeName} ${typeInfo.schema.structBody}\n@group(0) @binding(${bindingIndex}) var<storage, ${accessMode}> ${name}: array<${structTypeName}>;`;
|
|
4312
|
+
} else {
|
|
4313
|
+
declaration = `@group(0) @binding(${bindingIndex}) var<storage, ${accessMode}> ${name}: array<f32>;`;
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
if (isComputeShader) {
|
|
4317
|
+
strandsContext.computeDeclarations.add(declaration);
|
|
4318
|
+
} else {
|
|
4319
|
+
strandsContext.vertexDeclarations.add(declaration);
|
|
4320
|
+
strandsContext.fragmentDeclarations.add(declaration);
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
bindingIndex += 1;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
},
|
|
3857
4327
|
getTypeName(baseType, dimension) {
|
|
3858
4328
|
const primitiveTypeName = TypeNames[baseType + dimension];
|
|
3859
4329
|
if (!primitiveTypeName) {
|
|
@@ -3861,6 +4331,19 @@ const wgslBackend = {
|
|
|
3861
4331
|
}
|
|
3862
4332
|
return primitiveTypeName;
|
|
3863
4333
|
},
|
|
4334
|
+
getNoiseShaderSnippet() {
|
|
4335
|
+
return noiseWGSL;
|
|
4336
|
+
},
|
|
4337
|
+
getRandomFragmentShaderSnippet() {
|
|
4338
|
+
return randomWGSL;
|
|
4339
|
+
},
|
|
4340
|
+
getRandomVertexShaderSnippet() {
|
|
4341
|
+
return randomVertWGSL;
|
|
4342
|
+
},
|
|
4343
|
+
getRandomComputeShaderSnippet() {
|
|
4344
|
+
return randomComputeWGSL;
|
|
4345
|
+
},
|
|
4346
|
+
|
|
3864
4347
|
generateHookUniformKey(name, typeInfo) {
|
|
3865
4348
|
// For sampler2D types, we don't add them to the uniform struct,
|
|
3866
4349
|
// but we still need them in the shader's hooks object so that
|
|
@@ -3868,6 +4351,11 @@ const wgslBackend = {
|
|
|
3868
4351
|
if (typeInfo.baseType === 'sampler2D') {
|
|
3869
4352
|
return `${name}: sampler2D`; // Signal that this should not be added to uniform struct
|
|
3870
4353
|
}
|
|
4354
|
+
// For storage buffers, we don't add them to the uniform struct
|
|
4355
|
+
// Instead, they become separate storage buffer bindings
|
|
4356
|
+
if (typeInfo.baseType === 'storage') {
|
|
4357
|
+
return null; // Signal that this should not be added to uniform struct
|
|
4358
|
+
}
|
|
3871
4359
|
return `${name}: ${this.getTypeName(typeInfo.baseType, typeInfo.dimension)}`;
|
|
3872
4360
|
},
|
|
3873
4361
|
generateVaryingVariable(varName, typeInfo) {
|
|
@@ -3894,9 +4382,13 @@ const wgslBackend = {
|
|
|
3894
4382
|
// Generate just a semicolon (unless suppressed)
|
|
3895
4383
|
generationContext.write(semicolon);
|
|
3896
4384
|
} else if (node.statementType === StatementType.EARLY_RETURN) {
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
4385
|
+
if (node.dependsOn && node.dependsOn.length > 0) {
|
|
4386
|
+
const exprNodeID = node.dependsOn[0];
|
|
4387
|
+
const expr = this.generateExpression(generationContext, dag, exprNodeID);
|
|
4388
|
+
generationContext.write(`return ${expr}${semicolon}`);
|
|
4389
|
+
} else {
|
|
4390
|
+
generationContext.write(`return${semicolon}`);
|
|
4391
|
+
}
|
|
3900
4392
|
}
|
|
3901
4393
|
},
|
|
3902
4394
|
generateAssignment(generationContext, dag, nodeID) {
|
|
@@ -3908,6 +4400,17 @@ const wgslBackend = {
|
|
|
3908
4400
|
const targetNode = getNodeDataFromID(dag, targetNodeID);
|
|
3909
4401
|
const semicolon = generationContext.suppressSemicolon ? '' : ';';
|
|
3910
4402
|
|
|
4403
|
+
// Check if target is an array access (storage buffer assignment)
|
|
4404
|
+
if (targetNode.opCode === OpCode.Binary.ARRAY_ACCESS) {
|
|
4405
|
+
const [bufferID, indexID] = targetNode.dependsOn;
|
|
4406
|
+
const bufferExpr = this.generateExpression(generationContext, dag, bufferID);
|
|
4407
|
+
const indexExpr = this.generateExpression(generationContext, dag, indexID);
|
|
4408
|
+
const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID);
|
|
4409
|
+
const fieldSuffix = targetNode.identifier ? `.${targetNode.identifier}` : '';
|
|
4410
|
+
generationContext.write(`${bufferExpr}[i32(${indexExpr})]${fieldSuffix} = ${sourceExpr}${semicolon}`);
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
4413
|
+
|
|
3911
4414
|
// Check if target is a swizzle assignment
|
|
3912
4415
|
if (targetNode.opCode === OpCode.Unary.SWIZZLE) {
|
|
3913
4416
|
const parentID = targetNode.dependsOn[0];
|
|
@@ -3965,6 +4468,10 @@ const wgslBackend = {
|
|
|
3965
4468
|
return `var ${tmp}: ${typeName} = ${expr};`;
|
|
3966
4469
|
},
|
|
3967
4470
|
generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) {
|
|
4471
|
+
if (!returnType) {
|
|
4472
|
+
generationContext.write('return;');
|
|
4473
|
+
return;
|
|
4474
|
+
}
|
|
3968
4475
|
const dag = strandsContext.dag;
|
|
3969
4476
|
const rootNode = getNodeDataFromID(dag, rootNodeID);
|
|
3970
4477
|
if (isStructType(returnType)) {
|
|
@@ -4005,9 +4512,15 @@ const wgslBackend = {
|
|
|
4005
4512
|
}
|
|
4006
4513
|
}
|
|
4007
4514
|
|
|
4008
|
-
//
|
|
4515
|
+
// Detect instanceID usage in fragment context and rewrite to varying name
|
|
4516
|
+
if (node.identifier === this.instanceIdReference() && generationContext.shaderContext === 'fragment') {
|
|
4517
|
+
generationContext.strandsContext._instanceIDUsedInFragment = true;
|
|
4518
|
+
return INSTANCE_ID_VARYING_NAME;
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
// Check if this is a uniform variable (but not a texture or storage buffer)
|
|
4009
4522
|
const uniform = generationContext.strandsContext?.uniforms?.find(uniform => uniform.name === node.identifier);
|
|
4010
|
-
if (uniform && uniform.typeInfo.baseType !== 'sampler2D') {
|
|
4523
|
+
if (uniform && uniform.typeInfo.baseType !== 'sampler2D' && uniform.typeInfo.baseType !== 'storage') {
|
|
4011
4524
|
return `hooks.${node.identifier}`;
|
|
4012
4525
|
}
|
|
4013
4526
|
|
|
@@ -4026,6 +4539,13 @@ const wgslBackend = {
|
|
|
4026
4539
|
const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep));
|
|
4027
4540
|
return `${T}(${deps.join(', ')})`;
|
|
4028
4541
|
}
|
|
4542
|
+
if (node.opCode === OpCode.Nary.TERNARY) {
|
|
4543
|
+
const [condID, trueID, falseID] = node.dependsOn;
|
|
4544
|
+
const cond = this.generateExpression(generationContext, dag, condID);
|
|
4545
|
+
const trueExpr = this.generateExpression(generationContext, dag, trueID);
|
|
4546
|
+
const falseExpr = this.generateExpression(generationContext, dag, falseID);
|
|
4547
|
+
return `select(${falseExpr}, ${trueExpr}, ${cond})`;
|
|
4548
|
+
}
|
|
4029
4549
|
if (node.opCode === OpCode.Nary.FUNCTION_CALL) {
|
|
4030
4550
|
// Convert mod() function calls to % operator in WGSL
|
|
4031
4551
|
if (node.identifier === 'mod' && node.dependsOn.length === 2) {
|
|
@@ -4047,6 +4567,18 @@ const wgslBackend = {
|
|
|
4047
4567
|
}
|
|
4048
4568
|
|
|
4049
4569
|
const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg));
|
|
4570
|
+
|
|
4571
|
+
if (node.identifier === 'random') {
|
|
4572
|
+
const ctx = generationContext.shaderContext;
|
|
4573
|
+
if (ctx === 'fragment') {
|
|
4574
|
+
functionArgs.push('_p5FragPos.xy');
|
|
4575
|
+
} else if (ctx === 'vertex') {
|
|
4576
|
+
functionArgs.push('f32(_p5VertexId)');
|
|
4577
|
+
} else if (ctx === 'compute') {
|
|
4578
|
+
functionArgs.push('_p5GlobalId');
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4050
4582
|
return `${node.identifier}(${functionArgs.join(', ')})`;
|
|
4051
4583
|
}
|
|
4052
4584
|
if (node.opCode === OpCode.Binary.MEMBER_ACCESS) {
|
|
@@ -4060,6 +4592,13 @@ const wgslBackend = {
|
|
|
4060
4592
|
const parentExpr = this.generateExpression(generationContext, dag, parentID);
|
|
4061
4593
|
return `${parentExpr}.${node.swizzle}`;
|
|
4062
4594
|
}
|
|
4595
|
+
if (node.opCode === OpCode.Binary.ARRAY_ACCESS) {
|
|
4596
|
+
const [bufferID, indexID] = node.dependsOn;
|
|
4597
|
+
const bufferExpr = this.generateExpression(generationContext, dag, bufferID);
|
|
4598
|
+
const indexExpr = this.generateExpression(generationContext, dag, indexID);
|
|
4599
|
+
const fieldSuffix = node.identifier ? `.${node.identifier}` : '';
|
|
4600
|
+
return `${bufferExpr}[i32(${indexExpr})]${fieldSuffix}`;
|
|
4601
|
+
}
|
|
4063
4602
|
if (node.dependsOn.length === 2) {
|
|
4064
4603
|
const [lID, rID] = node.dependsOn;
|
|
4065
4604
|
const left = this.generateExpression(generationContext, dag, lID);
|
|
@@ -4133,12 +4672,25 @@ const wgslBackend = {
|
|
|
4133
4672
|
const samplerVariable = variableNode(strandsContext, { baseType: BaseType.SAMPLER, dimension: 1 }, samplerIdentifier);
|
|
4134
4673
|
const samplerNode = createStrandsNode(samplerVariable.id, samplerVariable.dimension, strandsContext);
|
|
4135
4674
|
|
|
4136
|
-
// Create
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4675
|
+
// Create a LOD literal node (0.0) so we can use textureSampleLevel instead
|
|
4676
|
+
// of textureSample. textureSample doesn't let you use uniform values in control
|
|
4677
|
+
// flow, whereas textureSampleLevel does. While we don't have mipmaps, we don't
|
|
4678
|
+
// miss out.
|
|
4679
|
+
// TODO: if we *do* add mipmap support, update this logic -- we'd need to hoist
|
|
4680
|
+
// the texture lookup out of the control flow.
|
|
4681
|
+
const lodLiteral = scalarLiteralNode(
|
|
4682
|
+
strandsContext,
|
|
4683
|
+
{ dimension: 1, baseType: BaseType.FLOAT },
|
|
4684
|
+
0.0
|
|
4685
|
+
);
|
|
4686
|
+
const lodNode = createStrandsNode(lodLiteral.id, lodLiteral.dimension, strandsContext);
|
|
4687
|
+
|
|
4688
|
+
// Create the augmented args: [texture, sampler, coords, lod]
|
|
4689
|
+
const augmentedArgs = [textureArg, samplerNode, coordsArg, lodNode];
|
|
4690
|
+
|
|
4691
|
+
const { id, dimension } = functionCallNode(strandsContext, 'textureSampleLevel', augmentedArgs, {
|
|
4140
4692
|
overloads: [{
|
|
4141
|
-
params: [DataType.sampler2D, DataType.sampler, DataType.float2],
|
|
4693
|
+
params: [DataType.sampler2D, DataType.sampler, DataType.float2, DataType.float1],
|
|
4142
4694
|
returnType: DataType.float4
|
|
4143
4695
|
}]
|
|
4144
4696
|
});
|
|
@@ -4148,114 +4700,11 @@ const wgslBackend = {
|
|
|
4148
4700
|
instanceIdReference() {
|
|
4149
4701
|
return 'instanceID';
|
|
4150
4702
|
},
|
|
4151
|
-
};
|
|
4152
|
-
|
|
4153
|
-
// Based on https://github.com/stegu/webgl-noise/blob/22434e04d7753f7e949e8d724ab3da2864c17a0f/src/noise3D.glsl
|
|
4154
|
-
// MIT licensed, adapted for p5.strands and converted to WGSL
|
|
4155
|
-
|
|
4156
|
-
var noiseWGSL = `fn mod289Vec3(x: vec3<f32>) -> vec3<f32> {
|
|
4157
|
-
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
|
4158
|
-
}
|
|
4159
|
-
|
|
4160
|
-
fn mod289Vec4(x: vec4<f32>) -> vec4<f32> {
|
|
4161
|
-
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
|
4162
|
-
}
|
|
4163
|
-
|
|
4164
|
-
fn permute(x: vec4<f32>) -> vec4<f32> {
|
|
4165
|
-
return mod289Vec4(((x*34.0)+10.0)*x);
|
|
4166
|
-
}
|
|
4167
|
-
|
|
4168
|
-
fn taylorInvSqrt(r: vec4<f32>) -> vec4<f32> {
|
|
4169
|
-
return vec4<f32>(1.79284291400159) - vec4<f32>(0.85373472095314) * r;
|
|
4170
|
-
}
|
|
4171
4703
|
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
// First corner
|
|
4177
|
-
var i = floor(v + dot(v, C.yyy));
|
|
4178
|
-
let x0 = v - i + dot(i, C.xxx);
|
|
4179
|
-
|
|
4180
|
-
// Other corners
|
|
4181
|
-
let g = step(x0.yzx, x0.xyz);
|
|
4182
|
-
let l = vec3<f32>(1.0) - g;
|
|
4183
|
-
let i1 = min(g.xyz, l.zxy);
|
|
4184
|
-
let i2 = max(g.xyz, l.zxy);
|
|
4185
|
-
|
|
4186
|
-
// x0 = x0 - 0.0 + 0.0 * C.xxx;
|
|
4187
|
-
// x1 = x0 - i1 + 1.0 * C.xxx;
|
|
4188
|
-
// x2 = x0 - i2 + 2.0 * C.xxx;
|
|
4189
|
-
// x3 = x0 - 1.0 + 3.0 * C.xxx;
|
|
4190
|
-
let x1 = x0 - i1 + C.xxx;
|
|
4191
|
-
let x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
|
|
4192
|
-
let x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
|
|
4193
|
-
|
|
4194
|
-
// Permutations
|
|
4195
|
-
i = mod289Vec3(i);
|
|
4196
|
-
let p = permute( permute( permute(
|
|
4197
|
-
i.z + vec4<f32>(0.0, i1.z, i2.z, 1.0 ))
|
|
4198
|
-
+ i.y + vec4<f32>(0.0, i1.y, i2.y, 1.0 ))
|
|
4199
|
-
+ i.x + vec4<f32>(0.0, i1.x, i2.x, 1.0 ));
|
|
4200
|
-
|
|
4201
|
-
// Gradients: 7x7 points over a square, mapped onto an octahedron.
|
|
4202
|
-
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
|
|
4203
|
-
let n_ = 0.142857142857; // 1.0/7.0
|
|
4204
|
-
let ns = n_ * D.wyz - D.xzx;
|
|
4205
|
-
|
|
4206
|
-
let j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
|
|
4207
|
-
|
|
4208
|
-
let x_ = floor(j * ns.z);
|
|
4209
|
-
let y_ = floor(j - 7.0 * x_ ); // mod(j,N)
|
|
4210
|
-
|
|
4211
|
-
let x = x_ *ns.x + ns.yyyy;
|
|
4212
|
-
let y = y_ *ns.x + ns.yyyy;
|
|
4213
|
-
let h = vec4<f32>(1.0) - abs(x) - abs(y);
|
|
4214
|
-
|
|
4215
|
-
let b0 = vec4<f32>( x.xy, y.xy );
|
|
4216
|
-
let b1 = vec4<f32>( x.zw, y.zw );
|
|
4217
|
-
|
|
4218
|
-
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
|
|
4219
|
-
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
|
|
4220
|
-
let s0 = floor(b0)*2.0 + vec4<f32>(1.0);
|
|
4221
|
-
let s1 = floor(b1)*2.0 + vec4<f32>(1.0);
|
|
4222
|
-
let sh = -step(h, vec4<f32>(0.0));
|
|
4223
|
-
|
|
4224
|
-
let a0 = b0.xzyw + s0.xzyw*sh.xxyy;
|
|
4225
|
-
let a1 = b1.xzyw + s1.xzyw*sh.zzww;
|
|
4226
|
-
|
|
4227
|
-
let p0 = vec3<f32>(a0.xy, h.x);
|
|
4228
|
-
let p1 = vec3<f32>(a0.zw, h.y);
|
|
4229
|
-
let p2 = vec3<f32>(a1.xy, h.z);
|
|
4230
|
-
let p3 = vec3<f32>(a1.zw, h.w);
|
|
4231
|
-
|
|
4232
|
-
//Normalise gradients
|
|
4233
|
-
let norm = taylorInvSqrt(vec4<f32>(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
|
4234
|
-
let p0_norm = p0 * norm.x;
|
|
4235
|
-
let p1_norm = p1 * norm.y;
|
|
4236
|
-
let p2_norm = p2 * norm.z;
|
|
4237
|
-
let p3_norm = p3 * norm.w;
|
|
4238
|
-
|
|
4239
|
-
// Mix final noise value
|
|
4240
|
-
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));
|
|
4241
|
-
m = m * m;
|
|
4242
|
-
return 105.0 * dot( m*m, vec4<f32>( dot(p0_norm,x0), dot(p1_norm,x1),
|
|
4243
|
-
dot(p2_norm,x2), dot(p3_norm,x3) ) );
|
|
4244
|
-
}
|
|
4245
|
-
|
|
4246
|
-
fn noise(st: vec3<f32>, octaves: i32, ampFalloff: f32) -> f32 {
|
|
4247
|
-
var result = 0.0;
|
|
4248
|
-
var amplitude = 1.0;
|
|
4249
|
-
var frequency = 1.0;
|
|
4250
|
-
|
|
4251
|
-
for (var i = 0; i < 8; i++) {
|
|
4252
|
-
if (i >= octaves) { break; }
|
|
4253
|
-
result += amplitude * baseNoise(st * frequency);
|
|
4254
|
-
frequency *= 2.0;
|
|
4255
|
-
amplitude *= ampFalloff;
|
|
4256
|
-
}
|
|
4257
|
-
return (result + 1.0) * 0.5;
|
|
4258
|
-
}`;
|
|
4704
|
+
generateInstanceIDVarying() {
|
|
4705
|
+
return { name: INSTANCE_ID_VARYING_NAME, declaration: `${INSTANCE_ID_VARYING_NAME}: i32`, source: 'i32(instanceID)', interpolation: 'flat' };
|
|
4706
|
+
},
|
|
4707
|
+
};
|
|
4259
4708
|
|
|
4260
4709
|
const filterUniforms = `
|
|
4261
4710
|
// Group 0: Filter Properties
|
|
@@ -4567,6 +5016,44 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
4567
5016
|
}
|
|
4568
5017
|
`;
|
|
4569
5018
|
|
|
5019
|
+
const baseComputeShader = `
|
|
5020
|
+
struct ComputeUniforms {
|
|
5021
|
+
uTotalCount: vec3<i32>,
|
|
5022
|
+
uPhysicalCount: vec3<i32>,
|
|
5023
|
+
}
|
|
5024
|
+
@group(0) @binding(0) var<uniform> uniforms: ComputeUniforms;
|
|
5025
|
+
|
|
5026
|
+
@compute @workgroup_size(8, 8, 1)
|
|
5027
|
+
fn main(
|
|
5028
|
+
@builtin(global_invocation_id) globalId: vec3<u32>,
|
|
5029
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
5030
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>,
|
|
5031
|
+
@builtin(local_invocation_index) localIndex: u32
|
|
5032
|
+
) {
|
|
5033
|
+
let totalIterations = u32(uniforms.uTotalCount.x) * u32(uniforms.uTotalCount.y) * u32(uniforms.uTotalCount.z);
|
|
5034
|
+
let physicalId = globalId.x + globalId.y * (u32(uniforms.uPhysicalCount.x)) + globalId.z * (u32(uniforms.uPhysicalCount.x) * u32(uniforms.uPhysicalCount.y));
|
|
5035
|
+
|
|
5036
|
+
if (physicalId >= totalIterations) {
|
|
5037
|
+
return;
|
|
5038
|
+
}
|
|
5039
|
+
|
|
5040
|
+
var index = vec3<i32>(0);
|
|
5041
|
+
index.x = i32(physicalId % u32(uniforms.uTotalCount.x));
|
|
5042
|
+
let remainingY = physicalId / u32(uniforms.uTotalCount.x);
|
|
5043
|
+
index.y = i32(remainingY % u32(uniforms.uTotalCount.y));
|
|
5044
|
+
index.z = i32(remainingY / u32(uniforms.uTotalCount.y));
|
|
5045
|
+
|
|
5046
|
+
HOOK_iteration(index);
|
|
5047
|
+
}
|
|
5048
|
+
`;
|
|
5049
|
+
|
|
5050
|
+
/**
|
|
5051
|
+
* @module 3D
|
|
5052
|
+
* @submodule p5.strands
|
|
5053
|
+
* @for p5
|
|
5054
|
+
*/
|
|
5055
|
+
|
|
5056
|
+
|
|
4570
5057
|
const FRAME_STATE = {
|
|
4571
5058
|
PENDING: 0,
|
|
4572
5059
|
UNPROMOTED: 1,
|
|
@@ -4588,6 +5075,338 @@ function rendererWebGPU(p5, fn) {
|
|
|
4588
5075
|
RGBA,
|
|
4589
5076
|
} = p5;
|
|
4590
5077
|
|
|
5078
|
+
class StorageBuffer {
|
|
5079
|
+
constructor(buffer, size, renderer, schema = null) {
|
|
5080
|
+
this._isStorageBuffer = true;
|
|
5081
|
+
this.buffer = buffer;
|
|
5082
|
+
this.size = size;
|
|
5083
|
+
this._renderer = renderer;
|
|
5084
|
+
this._schema = schema;
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
/**
|
|
5088
|
+
* Updates the data in the buffer with new values. The new data must be in
|
|
5089
|
+
* the same format as the data originally passed to
|
|
5090
|
+
* <a href="#/p5/createStorage">`createStorage()`</a>.
|
|
5091
|
+
*
|
|
5092
|
+
* ```js example
|
|
5093
|
+
* let particles;
|
|
5094
|
+
* let computeShader;
|
|
5095
|
+
* let displayShader;
|
|
5096
|
+
* let instance;
|
|
5097
|
+
* const numParticles = 100;
|
|
5098
|
+
*
|
|
5099
|
+
* async function setup() {
|
|
5100
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
5101
|
+
* particles = createStorage(makeParticles(width / 2, height / 2));
|
|
5102
|
+
* computeShader = buildComputeShader(simulate);
|
|
5103
|
+
* displayShader = buildMaterialShader(display);
|
|
5104
|
+
* instance = buildGeometry(drawParticle);
|
|
5105
|
+
* describe('100 orange particles shooting outward.');
|
|
5106
|
+
* }
|
|
5107
|
+
*
|
|
5108
|
+
* function makeParticles(x, y) {
|
|
5109
|
+
* let data = [];
|
|
5110
|
+
* for (let i = 0; i < numParticles; i++) {
|
|
5111
|
+
* let angle = (i / numParticles) * TWO_PI;
|
|
5112
|
+
* let speed = random(0.5, 2);
|
|
5113
|
+
* data.push({
|
|
5114
|
+
* position: createVector(x, y),
|
|
5115
|
+
* velocity: createVector(cos(angle) * speed, sin(angle) * speed),
|
|
5116
|
+
* });
|
|
5117
|
+
* }
|
|
5118
|
+
* return data;
|
|
5119
|
+
* }
|
|
5120
|
+
*
|
|
5121
|
+
* function drawParticle() {
|
|
5122
|
+
* sphere(2);
|
|
5123
|
+
* }
|
|
5124
|
+
*
|
|
5125
|
+
* function simulate() {
|
|
5126
|
+
* let data = uniformStorage(particles);
|
|
5127
|
+
* let idx = index.x;
|
|
5128
|
+
* data[idx].position = data[idx].position + data[idx].velocity;
|
|
5129
|
+
* }
|
|
5130
|
+
*
|
|
5131
|
+
* function display() {
|
|
5132
|
+
* let data = uniformStorage(particles);
|
|
5133
|
+
* worldInputs.begin();
|
|
5134
|
+
* let pos = data[instanceID()].position;
|
|
5135
|
+
* worldInputs.position.xy += pos - [width / 2, height / 2];
|
|
5136
|
+
* worldInputs.end();
|
|
5137
|
+
* }
|
|
5138
|
+
*
|
|
5139
|
+
* function draw() {
|
|
5140
|
+
* background(30);
|
|
5141
|
+
* if (frameCount % 60 === 0) {
|
|
5142
|
+
* particles.update(makeParticles(random(width), random(height)));
|
|
5143
|
+
* }
|
|
5144
|
+
* compute(computeShader, numParticles);
|
|
5145
|
+
* noStroke();
|
|
5146
|
+
* fill(255, 200, 50);
|
|
5147
|
+
* shader(displayShader);
|
|
5148
|
+
* model(instance, numParticles);
|
|
5149
|
+
* }
|
|
5150
|
+
* ```
|
|
5151
|
+
*
|
|
5152
|
+
* @method update
|
|
5153
|
+
* @for p5.StorageBuffer
|
|
5154
|
+
* @beta
|
|
5155
|
+
* @webgpu
|
|
5156
|
+
* @webgpuOnly
|
|
5157
|
+
* @param {Number[]|Float32Array|Object[]} data The new data to write into the buffer.
|
|
5158
|
+
*/
|
|
5159
|
+
update(data) {
|
|
5160
|
+
const device = this._renderer.device;
|
|
5161
|
+
|
|
5162
|
+
if (this._schema !== null) {
|
|
5163
|
+
// Buffer was created with a struct array
|
|
5164
|
+
if (
|
|
5165
|
+
!Array.isArray(data) ||
|
|
5166
|
+
data.length === 0 ||
|
|
5167
|
+
typeof data[0] !== 'object' ||
|
|
5168
|
+
Array.isArray(data[0])
|
|
5169
|
+
) {
|
|
5170
|
+
throw new Error(
|
|
5171
|
+
'update() expects an array of objects matching the original struct format'
|
|
5172
|
+
);
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5175
|
+
const newSchema = this._renderer._inferStructSchema(data[0]);
|
|
5176
|
+
if (newSchema.structBody !== this._schema.structBody) {
|
|
5177
|
+
throw new Error(
|
|
5178
|
+
`update() data structure doesn't match the original.\n` +
|
|
5179
|
+
` Expected: ${this._schema.structBody}\n` +
|
|
5180
|
+
` Got: ${newSchema.structBody}`
|
|
5181
|
+
);
|
|
5182
|
+
}
|
|
5183
|
+
|
|
5184
|
+
const packed = this._renderer._packStructArray(data, this._schema);
|
|
5185
|
+
if (packed.byteLength > this.size) {
|
|
5186
|
+
throw new Error(
|
|
5187
|
+
`update() data (${packed.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
|
|
5188
|
+
);
|
|
5189
|
+
}
|
|
5190
|
+
device.queue.writeBuffer(this.buffer, 0, packed);
|
|
5191
|
+
} else {
|
|
5192
|
+
// Buffer was created with a float array
|
|
5193
|
+
let floatData;
|
|
5194
|
+
if (data instanceof Float32Array) {
|
|
5195
|
+
floatData = data;
|
|
5196
|
+
} else if (Array.isArray(data)) {
|
|
5197
|
+
floatData = new Float32Array(data);
|
|
5198
|
+
} else {
|
|
5199
|
+
throw new Error(
|
|
5200
|
+
'update() expects a Float32Array or array of numbers for this buffer'
|
|
5201
|
+
);
|
|
5202
|
+
}
|
|
5203
|
+
|
|
5204
|
+
if (floatData.byteLength > this.size) {
|
|
5205
|
+
throw new Error(
|
|
5206
|
+
`update() data (${floatData.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
|
|
5207
|
+
);
|
|
5208
|
+
}
|
|
5209
|
+
device.queue.writeBuffer(this.buffer, 0, floatData);
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
/**
|
|
5214
|
+
* Reads data from a storage buffer back into JavaScript.
|
|
5215
|
+
*
|
|
5216
|
+
* Copies data from the GPU to the CPU using a temporary buffer,
|
|
5217
|
+
* so it must be awaited. Returns a `Float32Array` for number
|
|
5218
|
+
* buffers, or an array of plain objects for struct buffers.
|
|
5219
|
+
*
|
|
5220
|
+
* Note: This is a GPU -> CPU read, so calling it often (like every frame)
|
|
5221
|
+
* can be slow.
|
|
5222
|
+
*
|
|
5223
|
+
* ```js example
|
|
5224
|
+
* let data;
|
|
5225
|
+
* let computeShader;
|
|
5226
|
+
*
|
|
5227
|
+
* async function setup() {
|
|
5228
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
5229
|
+
*
|
|
5230
|
+
* data = createStorage(new Float32Array([1, 2, 3, 4]));
|
|
5231
|
+
* computeShader = buildComputeShader(doubleValues);
|
|
5232
|
+
* compute(computeShader, 4);
|
|
5233
|
+
*
|
|
5234
|
+
* let result = await data.read();
|
|
5235
|
+
* // result is Float32Array [2, 4, 6, 8]
|
|
5236
|
+
* for (let i = 0; i < result.length; i++) {
|
|
5237
|
+
* print(result[i]);
|
|
5238
|
+
* }
|
|
5239
|
+
* describe('Prints the values 2, 4, 6, 8 to the console.');
|
|
5240
|
+
* }
|
|
5241
|
+
*
|
|
5242
|
+
* function doubleValues() {
|
|
5243
|
+
* let d = uniformStorage(data);
|
|
5244
|
+
* let idx = index.x;
|
|
5245
|
+
* d[idx] = d[idx] * 2;
|
|
5246
|
+
* }
|
|
5247
|
+
* ```
|
|
5248
|
+
*
|
|
5249
|
+
* @method read
|
|
5250
|
+
* @for p5.StorageBuffer
|
|
5251
|
+
* @beta
|
|
5252
|
+
* @webgpu
|
|
5253
|
+
* @webgpuOnly
|
|
5254
|
+
* @returns {Promise<Float32Array|Object[]>}
|
|
5255
|
+
*/
|
|
5256
|
+
async read() {
|
|
5257
|
+
const device = this._renderer.device;
|
|
5258
|
+
this._renderer.flushDraw();
|
|
5259
|
+
|
|
5260
|
+
const stagingBuffer = device.createBuffer({
|
|
5261
|
+
size: this.size,
|
|
5262
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
5263
|
+
});
|
|
5264
|
+
|
|
5265
|
+
const commandEncoder = device.createCommandEncoder();
|
|
5266
|
+
commandEncoder.copyBufferToBuffer(this.buffer, 0, stagingBuffer, 0, this.size);
|
|
5267
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
5268
|
+
|
|
5269
|
+
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, this.size);
|
|
5270
|
+
const mappedRange = stagingBuffer.getMappedRange(0, this.size);
|
|
5271
|
+
|
|
5272
|
+
// Copy before unmapping because mapped memory becomes invalid after unmap
|
|
5273
|
+
const rawCopy = new Float32Array(mappedRange.byteLength / 4);
|
|
5274
|
+
rawCopy.set(new Float32Array(mappedRange));
|
|
5275
|
+
|
|
5276
|
+
stagingBuffer.unmap();
|
|
5277
|
+
stagingBuffer.destroy();
|
|
5278
|
+
|
|
5279
|
+
if (this._schema !== null) {
|
|
5280
|
+
return this._renderer._unpackStructArray(rawCopy, this._schema);
|
|
5281
|
+
}
|
|
5282
|
+
return rawCopy;
|
|
5283
|
+
}
|
|
5284
|
+
|
|
5285
|
+
/**
|
|
5286
|
+
* Updates a single element in the buffer at a given index. Use this
|
|
5287
|
+
* when only a small number of elements need to change. If you need to
|
|
5288
|
+
* replace all the data at once, use
|
|
5289
|
+
* <a href="#/p5.StorageBuffer/update">`update()`</a> instead.
|
|
5290
|
+
*
|
|
5291
|
+
* ```js
|
|
5292
|
+
* let buf;
|
|
5293
|
+
*
|
|
5294
|
+
* async function setup() {
|
|
5295
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
5296
|
+
*
|
|
5297
|
+
* // Float buffer: update one value by index
|
|
5298
|
+
* buf = createStorage(new Float32Array([1, 2, 3, 4]));
|
|
5299
|
+
* buf.set(2, 9.5); // only index 2 changes → [1, 2, 9.5, 4]
|
|
5300
|
+
*
|
|
5301
|
+
* let result = await buf.read();
|
|
5302
|
+
* print(result[2]); // 9.5
|
|
5303
|
+
* describe('Prints 9.5 to the console.');
|
|
5304
|
+
* }
|
|
5305
|
+
* ```
|
|
5306
|
+
*
|
|
5307
|
+
* ```js
|
|
5308
|
+
* let particles;
|
|
5309
|
+
* const numParticles = 100;
|
|
5310
|
+
*
|
|
5311
|
+
* async function setup() {
|
|
5312
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
5313
|
+
* particles = createStorage(makeParticles());
|
|
5314
|
+
*
|
|
5315
|
+
* // Replace particle 42 without touching the others
|
|
5316
|
+
* particles.set(42, {
|
|
5317
|
+
* position: createVector(0, 0),
|
|
5318
|
+
* velocity: createVector(1, 0),
|
|
5319
|
+
* });
|
|
5320
|
+
*
|
|
5321
|
+
* // Read back to confirm the update
|
|
5322
|
+
* let result = await particles.read();
|
|
5323
|
+
* print(result[42].position.x, result[42].position.y); // 0, 0
|
|
5324
|
+
* describe('Prints the position of particle 42 after updating it.');
|
|
5325
|
+
* }
|
|
5326
|
+
*
|
|
5327
|
+
* function makeParticles() {
|
|
5328
|
+
* let data = [];
|
|
5329
|
+
* for (let i = 0; i < numParticles; i++) {
|
|
5330
|
+
* data.push({
|
|
5331
|
+
* position: createVector(random(width), random(height)),
|
|
5332
|
+
* velocity: createVector(random(-1, 1), random(-1, 1)),
|
|
5333
|
+
* });
|
|
5334
|
+
* }
|
|
5335
|
+
* return data;
|
|
5336
|
+
* }
|
|
5337
|
+
* ```
|
|
5338
|
+
*
|
|
5339
|
+
* @method set
|
|
5340
|
+
* @for p5.StorageBuffer
|
|
5341
|
+
* @beta
|
|
5342
|
+
* @webgpu
|
|
5343
|
+
* @webgpuOnly
|
|
5344
|
+
* @param {Number} index The zero-based index of the element to update.
|
|
5345
|
+
* @param {Number|Object} value The new value. Pass a number for float
|
|
5346
|
+
* buffers, or a plain object matching the original struct layout for
|
|
5347
|
+
* struct buffers.
|
|
5348
|
+
*/
|
|
5349
|
+
set(index, value) {
|
|
5350
|
+
const device = this._renderer.device;
|
|
5351
|
+
|
|
5352
|
+
if (this._schema !== null) {
|
|
5353
|
+
// buffer was created with an array of structs
|
|
5354
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
5355
|
+
throw new Error(
|
|
5356
|
+
'set() expects a plain object matching the original struct format for this buffer'
|
|
5357
|
+
);
|
|
5358
|
+
}
|
|
5359
|
+
|
|
5360
|
+
const { stride } = this._schema;
|
|
5361
|
+
const byteOffset = index * stride;
|
|
5362
|
+
|
|
5363
|
+
if (byteOffset + stride > this.size) {
|
|
5364
|
+
throw new Error(
|
|
5365
|
+
`set() index ${index} is out of bounds for this buffer ` +
|
|
5366
|
+
`(buffer holds ${Math.floor(this.size / stride)} elements)`
|
|
5367
|
+
);
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
// pack just this one element using the same logic as update()
|
|
5371
|
+
const packed = this._renderer._packStructArray([value], this._schema);
|
|
5372
|
+
// use packed.buffer (ArrayBuffer) so the size arg is always in bytes
|
|
5373
|
+
device.queue.writeBuffer(this.buffer, byteOffset, packed.buffer, 0, stride);
|
|
5374
|
+
} else {
|
|
5375
|
+
// buffer was created with a float array
|
|
5376
|
+
if (typeof value !== 'number') {
|
|
5377
|
+
throw new Error(
|
|
5378
|
+
'set() expects a number for this float buffer'
|
|
5379
|
+
);
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
const byteOffset = index * 4;
|
|
5383
|
+
|
|
5384
|
+
if (byteOffset + 4 > this.size) {
|
|
5385
|
+
throw new Error(
|
|
5386
|
+
`set() index ${index} is out of bounds for this buffer ` +
|
|
5387
|
+
`(buffer holds ${Math.floor(this.size / 4)} floats)`
|
|
5388
|
+
);
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5391
|
+
device.queue.writeBuffer(this.buffer, byteOffset, new Float32Array([value]));
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
5395
|
+
|
|
5396
|
+
/**
|
|
5397
|
+
* A block of data that shaders can read from, and compute shaders can also
|
|
5398
|
+
* write to. This is only available in WebGPU mode.
|
|
5399
|
+
*
|
|
5400
|
+
* Note: <a href="#/p5/createStorage">`createStorage()`</a> is the recommended
|
|
5401
|
+
* way to create an instance of this class.
|
|
5402
|
+
*
|
|
5403
|
+
* @class p5.StorageBuffer
|
|
5404
|
+
* @beta
|
|
5405
|
+
* @webgpu
|
|
5406
|
+
* @webgpuOnly
|
|
5407
|
+
*/
|
|
5408
|
+
p5.StorageBuffer = StorageBuffer;
|
|
5409
|
+
|
|
4591
5410
|
class RendererWebGPU extends Renderer3D {
|
|
4592
5411
|
constructor(pInst, w, h, isMainCanvas, elt) {
|
|
4593
5412
|
super(pInst, w, h, isMainCanvas, elt);
|
|
@@ -4639,6 +5458,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
4639
5458
|
// Retired buffers to destroy at end of frame
|
|
4640
5459
|
this._retiredBuffers = [];
|
|
4641
5460
|
|
|
5461
|
+
// Storage buffers for compute shaders
|
|
5462
|
+
this._storageBuffers = new Set();
|
|
5463
|
+
|
|
4642
5464
|
// 2D canvas for pixel reading fallback
|
|
4643
5465
|
this._pixelReadCanvas = null;
|
|
4644
5466
|
this._pixelReadCtx = null;
|
|
@@ -4715,7 +5537,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
4715
5537
|
}
|
|
4716
5538
|
if (this._pInst._webgpuAttributes[key] !== value) {
|
|
4717
5539
|
//changing value of previously altered attribute
|
|
4718
|
-
this._webgpuAttributes[key] = value;
|
|
5540
|
+
this._pInst._webgpuAttributes[key] = value;
|
|
4719
5541
|
unchanged = false;
|
|
4720
5542
|
}
|
|
4721
5543
|
//setting all attributes with some change
|
|
@@ -4849,9 +5671,21 @@ function rendererWebGPU(p5, fn) {
|
|
|
4849
5671
|
const _b = args[2] || 0;
|
|
4850
5672
|
const _a = args[3] || 0;
|
|
4851
5673
|
|
|
4852
|
-
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED
|
|
4853
|
-
if
|
|
4854
|
-
|
|
5674
|
+
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED.
|
|
5675
|
+
// However, if we are still in setup (frameCount == 0), we must promote
|
|
5676
|
+
// so that mainFramebuffer gets the cleared content. This ensures that if
|
|
5677
|
+
// draw() later promotes without a copy, it starts from the correct state
|
|
5678
|
+
// rather than a stale mainFramebuffer.
|
|
5679
|
+
// Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED
|
|
5680
|
+
// (i.e. calling background() some frames but not others) will still
|
|
5681
|
+
// lose intermediate UNPROMOTED frame content.
|
|
5682
|
+
if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) {
|
|
5683
|
+
if (this._pInst.frameCount > 0) {
|
|
5684
|
+
this._frameState = FRAME_STATE.UNPROMOTED;
|
|
5685
|
+
} else {
|
|
5686
|
+
this._promoteToFramebufferWithoutCopy();
|
|
5687
|
+
// clear() then targets mainFramebuffer via activeFramebuffer()
|
|
5688
|
+
}
|
|
4855
5689
|
}
|
|
4856
5690
|
|
|
4857
5691
|
this._finishActiveRenderPass();
|
|
@@ -5054,7 +5888,8 @@ function rendererWebGPU(p5, fn) {
|
|
|
5054
5888
|
return 4; // Cap at 4 for broader compatibility
|
|
5055
5889
|
}
|
|
5056
5890
|
|
|
5057
|
-
_shaderOptions({ mode }) {
|
|
5891
|
+
_shaderOptions({ mode, compute, workgroupSize }) {
|
|
5892
|
+
if (compute) return { compute: true, workgroupSize };
|
|
5058
5893
|
const activeFramebuffer = this.activeFramebuffer();
|
|
5059
5894
|
const format = activeFramebuffer ?
|
|
5060
5895
|
this._getWebGPUColorFormat(activeFramebuffer) :
|
|
@@ -5065,9 +5900,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
5065
5900
|
1; // No MSAA needed when blitting already-antialiased textures to canvas
|
|
5066
5901
|
const sampleCount = this._getValidSampleCount(requestedSampleCount);
|
|
5067
5902
|
|
|
5068
|
-
const depthFormat = activeFramebuffer
|
|
5069
|
-
this._getWebGPUDepthFormat(activeFramebuffer) :
|
|
5070
|
-
this.depthFormat;
|
|
5903
|
+
const depthFormat = activeFramebuffer
|
|
5904
|
+
? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined)
|
|
5905
|
+
: this.depthFormat;
|
|
5071
5906
|
|
|
5072
5907
|
const drawTarget = this.drawTarget();
|
|
5073
5908
|
const clipping = this._clipping;
|
|
@@ -5095,6 +5930,31 @@ function rendererWebGPU(p5, fn) {
|
|
|
5095
5930
|
_initShader(shader) {
|
|
5096
5931
|
const device = this.device;
|
|
5097
5932
|
|
|
5933
|
+
if (shader.shaderType === 'compute') {
|
|
5934
|
+
// Compute shader initialization
|
|
5935
|
+
shader.computeModule = device.createShaderModule({ code: shader.computeSrc() });
|
|
5936
|
+
shader._computePipelineCache = null;
|
|
5937
|
+
shader._workgroupSize = null;
|
|
5938
|
+
|
|
5939
|
+
// Create compute pipeline (deferred until first compute() call)
|
|
5940
|
+
shader.getPipeline = ({ workgroupSize }) => {
|
|
5941
|
+
if (!shader._computePipelineCache) {
|
|
5942
|
+
shader._computePipelineCache = device.createComputePipeline({
|
|
5943
|
+
layout: shader._pipelineLayout,
|
|
5944
|
+
compute: {
|
|
5945
|
+
module: shader.computeModule,
|
|
5946
|
+
entryPoint: 'main'
|
|
5947
|
+
}
|
|
5948
|
+
});
|
|
5949
|
+
shader._workgroupSize = workgroupSize;
|
|
5950
|
+
}
|
|
5951
|
+
return shader._computePipelineCache;
|
|
5952
|
+
};
|
|
5953
|
+
|
|
5954
|
+
return;
|
|
5955
|
+
}
|
|
5956
|
+
|
|
5957
|
+
// Render shader initialization
|
|
5098
5958
|
shader.vertModule = device.createShaderModule({ code: shader.vertSrc() });
|
|
5099
5959
|
shader.fragModule = device.createShaderModule({ code: shader.fragSrc() });
|
|
5100
5960
|
|
|
@@ -5119,25 +5979,27 @@ function rendererWebGPU(p5, fn) {
|
|
|
5119
5979
|
},
|
|
5120
5980
|
primitive: { topology },
|
|
5121
5981
|
multisample: { count: sampleCount },
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5982
|
+
...(depthFormat ? {
|
|
5983
|
+
depthStencil: {
|
|
5984
|
+
format: depthFormat,
|
|
5985
|
+
depthWriteEnabled: !clipping,
|
|
5986
|
+
depthCompare: 'less-equal',
|
|
5987
|
+
stencilFront: {
|
|
5988
|
+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
5989
|
+
failOp: 'keep',
|
|
5990
|
+
depthFailOp: 'keep',
|
|
5991
|
+
passOp: clipping ? 'replace' : 'keep',
|
|
5992
|
+
},
|
|
5993
|
+
stencilBack: {
|
|
5994
|
+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
5995
|
+
failOp: 'keep',
|
|
5996
|
+
depthFailOp: 'keep',
|
|
5997
|
+
passOp: clipping ? 'replace' : 'keep',
|
|
5998
|
+
},
|
|
5999
|
+
stencilReadMask: 0xFF,
|
|
6000
|
+
stencilWriteMask: clipping ? 0xFF : 0x00,
|
|
5131
6001
|
},
|
|
5132
|
-
|
|
5133
|
-
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
5134
|
-
failOp: 'keep',
|
|
5135
|
-
depthFailOp: 'keep',
|
|
5136
|
-
passOp: clipping ? 'replace' : 'keep',
|
|
5137
|
-
},
|
|
5138
|
-
stencilReadMask: 0xFF,
|
|
5139
|
-
stencilWriteMask: clipping ? 0xFF : 0x00,
|
|
5140
|
-
},
|
|
6002
|
+
} : {}),
|
|
5141
6003
|
});
|
|
5142
6004
|
shader._pipelineCache.set(key, pipeline);
|
|
5143
6005
|
}
|
|
@@ -5193,7 +6055,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
5193
6055
|
entries.push({
|
|
5194
6056
|
bufferGroup,
|
|
5195
6057
|
binding: bufferGroup.binding,
|
|
5196
|
-
visibility:
|
|
6058
|
+
visibility: shader.shaderType === 'compute'
|
|
6059
|
+
? GPUShaderStage.COMPUTE
|
|
6060
|
+
: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
5197
6061
|
buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
|
|
5198
6062
|
});
|
|
5199
6063
|
structEntries.set(bufferGroup.group, entries);
|
|
@@ -5227,6 +6091,24 @@ function rendererWebGPU(p5, fn) {
|
|
|
5227
6091
|
groupEntries.set(group, entries);
|
|
5228
6092
|
}
|
|
5229
6093
|
|
|
6094
|
+
// Add storage buffer bindings
|
|
6095
|
+
for (const storage of shader._storageBuffers || []) {
|
|
6096
|
+
const group = storage.group;
|
|
6097
|
+
const entries = groupEntries.get(group) || [];
|
|
6098
|
+
|
|
6099
|
+
entries.push({
|
|
6100
|
+
binding: storage.binding,
|
|
6101
|
+
visibility: storage.visibility,
|
|
6102
|
+
buffer: {
|
|
6103
|
+
type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage'
|
|
6104
|
+
},
|
|
6105
|
+
storage: storage,
|
|
6106
|
+
});
|
|
6107
|
+
|
|
6108
|
+
entries.sort((a, b) => a.binding - b.binding);
|
|
6109
|
+
groupEntries.set(group, entries);
|
|
6110
|
+
}
|
|
6111
|
+
|
|
5230
6112
|
// Create layouts and bind groups
|
|
5231
6113
|
const groupEntriesArr = [];
|
|
5232
6114
|
for (const [group, entries] of groupEntries) {
|
|
@@ -5245,6 +6127,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
5245
6127
|
shader._pipelineLayout = this.device.createPipelineLayout({
|
|
5246
6128
|
bindGroupLayouts: shader._bindGroupLayouts,
|
|
5247
6129
|
});
|
|
6130
|
+
shader._compiled = true;
|
|
5248
6131
|
}
|
|
5249
6132
|
|
|
5250
6133
|
_getBlendState(mode) {
|
|
@@ -5491,8 +6374,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
5491
6374
|
|
|
5492
6375
|
_resetBuffersBeforeDraw() {
|
|
5493
6376
|
this._finishActiveRenderPass();
|
|
6377
|
+
|
|
5494
6378
|
// Set state to PENDING - we'll decide on first draw
|
|
5495
|
-
this.
|
|
6379
|
+
if (this._pInst.frameCount > 0) {
|
|
6380
|
+
this._frameState = FRAME_STATE.PENDING;
|
|
6381
|
+
}
|
|
5496
6382
|
|
|
5497
6383
|
// Clear depth buffer but DON'T start any render pass yet
|
|
5498
6384
|
const activeFramebuffer = this.activeFramebuffer();
|
|
@@ -5603,6 +6489,8 @@ function rendererWebGPU(p5, fn) {
|
|
|
5603
6489
|
// once we're drawing to the framebuffer, because normally
|
|
5604
6490
|
// those are reset.
|
|
5605
6491
|
const savedModelMatrix = this.states.uModelMatrix.copy();
|
|
6492
|
+
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
|
|
6493
|
+
this.states.uModelMatrix.reset();
|
|
5606
6494
|
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
|
|
5607
6495
|
|
|
5608
6496
|
this.mainFramebuffer.begin();
|
|
@@ -5610,7 +6498,12 @@ function rendererWebGPU(p5, fn) {
|
|
|
5610
6498
|
this.states.uModelMatrix.set(savedModelMatrix);
|
|
5611
6499
|
}
|
|
5612
6500
|
|
|
5613
|
-
_promoteToFramebufferWithoutCopy() {
|
|
6501
|
+
_promoteToFramebufferWithoutCopy() {
|
|
6502
|
+
// Already promoted this frame
|
|
6503
|
+
if (this._frameState === FRAME_STATE.PROMOTED) {
|
|
6504
|
+
return;
|
|
6505
|
+
}
|
|
6506
|
+
|
|
5614
6507
|
// Ensure mainFramebuffer matches canvas size
|
|
5615
6508
|
if (this.mainFramebuffer.width !== this.width ||
|
|
5616
6509
|
this.mainFramebuffer.height !== this.height) {
|
|
@@ -5625,6 +6518,8 @@ function rendererWebGPU(p5, fn) {
|
|
|
5625
6518
|
|
|
5626
6519
|
// Preserve transformation state
|
|
5627
6520
|
const savedModelMatrix = this.states.uModelMatrix.copy();
|
|
6521
|
+
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
|
|
6522
|
+
this.states.uModelMatrix.reset();
|
|
5628
6523
|
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
|
|
5629
6524
|
|
|
5630
6525
|
// Begin rendering to mainFramebuffer
|
|
@@ -5938,7 +6833,6 @@ function rendererWebGPU(p5, fn) {
|
|
|
5938
6833
|
}
|
|
5939
6834
|
this.flushDraw();
|
|
5940
6835
|
|
|
5941
|
-
// this._pInst.background('red');
|
|
5942
6836
|
this._pInst.push();
|
|
5943
6837
|
this.states.setValue('enableLighting', false);
|
|
5944
6838
|
this.states.setValue('activeImageLight', null);
|
|
@@ -6003,25 +6897,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
6003
6897
|
|
|
6004
6898
|
this._beginActiveRenderPass();
|
|
6005
6899
|
const passEncoder = this.activeRenderPass;
|
|
6006
|
-
const currentShader = this._curShader;
|
|
6007
|
-
const shaderOptions = this._shaderOptions({ mode });
|
|
6008
|
-
if (this.activeShader !== currentShader || this._shaderOptionsDifferent(shaderOptions)) {
|
|
6009
|
-
passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
|
|
6010
|
-
}
|
|
6011
|
-
this.activeShader = currentShader;
|
|
6012
|
-
this.activeShaderOptions = shaderOptions;
|
|
6013
6900
|
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
if (drawTarget._isClipApplied && !this._clipping) {
|
|
6017
|
-
// When using the clip mask, test against reference value 0 (background)
|
|
6018
|
-
// WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
|
|
6019
|
-
// In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
|
|
6020
|
-
passEncoder.setStencilReference(0);
|
|
6021
|
-
} else if (this._clipping) {
|
|
6022
|
-
// When writing to the clip mask, write reference value 1
|
|
6023
|
-
passEncoder.setStencilReference(1);
|
|
6024
|
-
}
|
|
6901
|
+
const currentShader = this._curShader;
|
|
6902
|
+
this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers });
|
|
6025
6903
|
// Bind vertex buffers
|
|
6026
6904
|
for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
|
|
6027
6905
|
const location = currentShader.attributes[buffer.attr].location;
|
|
@@ -6029,6 +6907,58 @@ function rendererWebGPU(p5, fn) {
|
|
|
6029
6907
|
passEncoder.setVertexBuffer(location, gpuBuffer, 0);
|
|
6030
6908
|
}
|
|
6031
6909
|
|
|
6910
|
+
if (currentShader.shaderType === "fill") {
|
|
6911
|
+
// Bind index buffer and issue draw
|
|
6912
|
+
if (buffers.indexBuffer) {
|
|
6913
|
+
const indexFormat = buffers.indexFormat || "uint16";
|
|
6914
|
+
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
|
|
6915
|
+
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
|
|
6916
|
+
} else {
|
|
6917
|
+
passEncoder.draw(geometry.vertices.length, count, 0, 0);
|
|
6918
|
+
}
|
|
6919
|
+
} else if (currentShader.shaderType === "text") {
|
|
6920
|
+
if (!buffers.indexBuffer) {
|
|
6921
|
+
throw new Error("Text geometry must have an index buffer");
|
|
6922
|
+
}
|
|
6923
|
+
const indexFormat = buffers.indexFormat || "uint16";
|
|
6924
|
+
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
|
|
6925
|
+
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
|
|
6926
|
+
}
|
|
6927
|
+
|
|
6928
|
+
if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
|
|
6929
|
+
passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
|
|
6930
|
+
}
|
|
6931
|
+
|
|
6932
|
+
// Mark that we have pending draws that need submission
|
|
6933
|
+
this._hasPendingDraws = true;
|
|
6934
|
+
}
|
|
6935
|
+
|
|
6936
|
+
setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) {
|
|
6937
|
+
const shaderOptions = this._shaderOptions(shaderOptionsParams);
|
|
6938
|
+
if (
|
|
6939
|
+
shaderOptions.compute ||
|
|
6940
|
+
this.activeShader !== currentShader ||
|
|
6941
|
+
this._shaderOptionsDifferent(shaderOptions)
|
|
6942
|
+
) {
|
|
6943
|
+
passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
|
|
6944
|
+
}
|
|
6945
|
+
if (!shaderOptions.compute) {
|
|
6946
|
+
this.activeShader = currentShader;
|
|
6947
|
+
this.activeShaderOptions = shaderOptions;
|
|
6948
|
+
|
|
6949
|
+
// Set stencil reference value for clipping
|
|
6950
|
+
const drawTarget = this.drawTarget();
|
|
6951
|
+
if (drawTarget._isClipApplied && !this._clipping) {
|
|
6952
|
+
// When using the clip mask, test against reference value 0 (background)
|
|
6953
|
+
// WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
|
|
6954
|
+
// In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
|
|
6955
|
+
passEncoder.setStencilReference(0);
|
|
6956
|
+
} else if (this._clipping) {
|
|
6957
|
+
// When writing to the clip mask, write reference value 1
|
|
6958
|
+
passEncoder.setStencilReference(1);
|
|
6959
|
+
}
|
|
6960
|
+
}
|
|
6961
|
+
|
|
6032
6962
|
for (const bufferGroup of currentShader._uniformBufferGroups) {
|
|
6033
6963
|
if (bufferGroup.dynamic) {
|
|
6034
6964
|
// Bind uniforms into a part of a big dynamic memory block because
|
|
@@ -6081,6 +7011,13 @@ function rendererWebGPU(p5, fn) {
|
|
|
6081
7011
|
currentShader.buffersDirty.delete(key);
|
|
6082
7012
|
}
|
|
6083
7013
|
}
|
|
7014
|
+
for (const storage of currentShader._storageBuffers || []) {
|
|
7015
|
+
const key = storage.group * 1000 + storage.binding;
|
|
7016
|
+
if (currentShader.buffersDirty.has(key)) {
|
|
7017
|
+
currentShader._cachedBindGroup[storage.group] = undefined;
|
|
7018
|
+
currentShader.buffersDirty.delete(key);
|
|
7019
|
+
}
|
|
7020
|
+
}
|
|
6084
7021
|
|
|
6085
7022
|
// Bind sampler/texture uniforms and uniform buffers
|
|
6086
7023
|
for (const iter of currentShader._groupEntries) {
|
|
@@ -6110,6 +7047,19 @@ function rendererWebGPU(p5, fn) {
|
|
|
6110
7047
|
: { buffer: uniformBufferInfo.buffer },
|
|
6111
7048
|
});
|
|
6112
7049
|
}
|
|
7050
|
+
} else if (entry.storage && !bindGroup) {
|
|
7051
|
+
// Storage buffer binding
|
|
7052
|
+
const uniform = currentShader.uniforms[entry.storage.name];
|
|
7053
|
+
if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) {
|
|
7054
|
+
throw new Error(
|
|
7055
|
+
`Storage buffer "${entry.storage.name}" not set. ` +
|
|
7056
|
+
`Use shader.setUniform("${entry.storage.name}", storageBuffer)`
|
|
7057
|
+
);
|
|
7058
|
+
}
|
|
7059
|
+
bgEntries.push({
|
|
7060
|
+
binding: entry.binding,
|
|
7061
|
+
resource: { buffer: uniform._cachedData.buffer },
|
|
7062
|
+
});
|
|
6113
7063
|
} else if (!bindGroup) {
|
|
6114
7064
|
bgEntries.push({
|
|
6115
7065
|
binding: entry.binding,
|
|
@@ -6143,84 +7093,71 @@ function rendererWebGPU(p5, fn) {
|
|
|
6143
7093
|
);
|
|
6144
7094
|
}
|
|
6145
7095
|
}
|
|
7096
|
+
return passEncoder;
|
|
7097
|
+
}
|
|
6146
7098
|
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
7099
|
+
//////////////////////////////////////////////
|
|
7100
|
+
// SHADER
|
|
7101
|
+
//////////////////////////////////////////////
|
|
7102
|
+
|
|
7103
|
+
// Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset).
|
|
7104
|
+
//
|
|
7105
|
+
// Field interface (shared by uniform fields from _parseStruct and struct storage schema fields):
|
|
7106
|
+
// baseType: string - 'f32', 'i32', 'u32', etc.
|
|
7107
|
+
// size: number - byte size of the field
|
|
7108
|
+
// offset: number - byte offset of the field within its struct
|
|
7109
|
+
// packInPlace: bool - true for mat3, written with manual column padding
|
|
7110
|
+
//
|
|
7111
|
+
// value: number or number[] - the data to write
|
|
7112
|
+
_packField(field, value, floatView, dataView, baseOffset) {
|
|
7113
|
+
if (value === undefined) return;
|
|
7114
|
+
|
|
7115
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
7116
|
+
// copy of the Color/Vector classes
|
|
7117
|
+
if (value?.isVector) {
|
|
7118
|
+
value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values;
|
|
7119
|
+
} else if (value?.isColor) {
|
|
7120
|
+
value = value._getRGBA([1, 1, 1, 1]);
|
|
7121
|
+
}
|
|
7122
|
+
const byteOffset = baseOffset + field.offset;
|
|
7123
|
+
if (field.baseType === 'u32') {
|
|
7124
|
+
if (field.size === 4) {
|
|
7125
|
+
dataView.setUint32(byteOffset, value, true);
|
|
6153
7126
|
} else {
|
|
6154
|
-
|
|
7127
|
+
for (let i = 0; i < value.length; i++) {
|
|
7128
|
+
dataView.setUint32(byteOffset + i * 4, value[i], true);
|
|
7129
|
+
}
|
|
6155
7130
|
}
|
|
6156
|
-
} else if (
|
|
6157
|
-
if (
|
|
6158
|
-
|
|
7131
|
+
} else if (field.baseType === 'i32') {
|
|
7132
|
+
if (field.size === 4) {
|
|
7133
|
+
dataView.setInt32(byteOffset, value, true);
|
|
7134
|
+
} else {
|
|
7135
|
+
for (let i = 0; i < value.length; i++) {
|
|
7136
|
+
dataView.setInt32(byteOffset + i * 4, value[i], true);
|
|
7137
|
+
}
|
|
6159
7138
|
}
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
7139
|
+
} else if (field.packInPlace) {
|
|
7140
|
+
// In-place packing for mat3: write directly to buffer with padding
|
|
7141
|
+
const base = byteOffset / 4;
|
|
7142
|
+
floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2];
|
|
7143
|
+
floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5];
|
|
7144
|
+
floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8];
|
|
7145
|
+
} else if (field.size === 4) {
|
|
7146
|
+
floatView.set([value], byteOffset / 4);
|
|
7147
|
+
} else {
|
|
7148
|
+
floatView.set(value, byteOffset / 4);
|
|
6167
7149
|
}
|
|
6168
|
-
|
|
6169
|
-
// Mark that we have pending draws that need submission
|
|
6170
|
-
this._hasPendingDraws = true;
|
|
6171
7150
|
}
|
|
6172
7151
|
|
|
6173
|
-
//////////////////////////////////////////////
|
|
6174
|
-
// SHADER
|
|
6175
|
-
//////////////////////////////////////////////
|
|
6176
|
-
|
|
6177
7152
|
_packUniformGroup(shader, groupUniforms, bufferInfo) {
|
|
6178
7153
|
// Pack a single group's uniforms into a buffer
|
|
6179
7154
|
const data = bufferInfo.data;
|
|
6180
7155
|
const dataView = bufferInfo.dataView;
|
|
6181
|
-
|
|
6182
7156
|
const offset = bufferInfo.offset || 0;
|
|
6183
7157
|
for (const uniform of groupUniforms) {
|
|
6184
7158
|
const fullUniform = shader.uniforms[uniform.name];
|
|
6185
7159
|
if (!fullUniform || fullUniform.isSampler) continue;
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
if (fullUniform.baseType === 'u32') {
|
|
6189
|
-
if (fullUniform.size === 4) {
|
|
6190
|
-
dataView.setUint32(offset + fullUniform.offset, uniformData, true);
|
|
6191
|
-
} else {
|
|
6192
|
-
for (let i = 0; i < uniformData.length; i++) {
|
|
6193
|
-
dataView.setUint32(offset + fullUniform.offset + i * 4, uniformData[i], true);
|
|
6194
|
-
}
|
|
6195
|
-
}
|
|
6196
|
-
} else if (fullUniform.baseType === 'i32') {
|
|
6197
|
-
if (fullUniform.size === 4) {
|
|
6198
|
-
dataView.setInt32(offset + fullUniform.offset, uniformData, true);
|
|
6199
|
-
} else {
|
|
6200
|
-
for (let i = 0; i < uniformData.length; i++) {
|
|
6201
|
-
dataView.setInt32(offset + fullUniform.offset + i * 4, uniformData[i], true);
|
|
6202
|
-
}
|
|
6203
|
-
}
|
|
6204
|
-
} else if (fullUniform.packInPlace) {
|
|
6205
|
-
// In-place packing for mat3: write directly to buffer with padding
|
|
6206
|
-
const baseOffset = (offset + fullUniform.offset) / 4;
|
|
6207
|
-
// Column 0
|
|
6208
|
-
data[baseOffset + 0] = uniformData[0];
|
|
6209
|
-
data[baseOffset + 1] = uniformData[1];
|
|
6210
|
-
data[baseOffset + 2] = uniformData[2];
|
|
6211
|
-
// Column 1
|
|
6212
|
-
data[baseOffset + 4] = uniformData[3];
|
|
6213
|
-
data[baseOffset + 5] = uniformData[4];
|
|
6214
|
-
data[baseOffset + 6] = uniformData[5];
|
|
6215
|
-
// Column 2
|
|
6216
|
-
data[baseOffset + 8] = uniformData[6];
|
|
6217
|
-
data[baseOffset + 9] = uniformData[7];
|
|
6218
|
-
data[baseOffset + 10] = uniformData[8];
|
|
6219
|
-
} else if (fullUniform.size === 4) {
|
|
6220
|
-
data.set([uniformData], (offset + fullUniform.offset) / 4);
|
|
6221
|
-
} else if (uniformData !== undefined) {
|
|
6222
|
-
data.set(uniformData, (offset + fullUniform.offset) / 4);
|
|
6223
|
-
}
|
|
7160
|
+
this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset);
|
|
6224
7161
|
}
|
|
6225
7162
|
}
|
|
6226
7163
|
|
|
@@ -6367,10 +7304,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
6367
7304
|
const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
|
|
6368
7305
|
|
|
6369
7306
|
let match;
|
|
6370
|
-
|
|
7307
|
+
const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc();
|
|
7308
|
+
while ((match = uniformVarRegex.exec(src)) !== null) {
|
|
6371
7309
|
const [_, groupNum, binding, varName, structType] = match;
|
|
6372
7310
|
const bindingIndex = parseInt(binding);
|
|
6373
|
-
const uniforms = this._parseStruct(
|
|
7311
|
+
const uniforms = this._parseStruct(src, structType);
|
|
6374
7312
|
|
|
6375
7313
|
uniformGroups.push({
|
|
6376
7314
|
group: parseInt(groupNum),
|
|
@@ -6381,7 +7319,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
6381
7319
|
});
|
|
6382
7320
|
}
|
|
6383
7321
|
|
|
6384
|
-
if (uniformGroups.length === 0) {
|
|
7322
|
+
if (uniformGroups.length === 0 && shader.shaderType !== 'compute') {
|
|
6385
7323
|
throw new Error('Expected at least one uniform struct bound to @group(0)');
|
|
6386
7324
|
}
|
|
6387
7325
|
|
|
@@ -6408,6 +7346,10 @@ function rendererWebGPU(p5, fn) {
|
|
|
6408
7346
|
// TODO: support other texture types
|
|
6409
7347
|
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
|
|
6410
7348
|
|
|
7349
|
+
// Extract storage buffers
|
|
7350
|
+
const storageBuffers = {};
|
|
7351
|
+
const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g;
|
|
7352
|
+
|
|
6411
7353
|
// Track which bindings are taken by the struct properties we've parsed
|
|
6412
7354
|
// (the rest should be textures/samplers)
|
|
6413
7355
|
const structUniformBindings = {};
|
|
@@ -6417,8 +7359,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
6417
7359
|
|
|
6418
7360
|
for (const [src, visibility] of [
|
|
6419
7361
|
[shader.vertSrc(), GPUShaderStage.VERTEX],
|
|
6420
|
-
[shader.fragSrc(), GPUShaderStage.FRAGMENT]
|
|
7362
|
+
[shader.fragSrc(), GPUShaderStage.FRAGMENT],
|
|
7363
|
+
[shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE]
|
|
6421
7364
|
]) {
|
|
7365
|
+
if (!src) continue; // Skip if shader stage doesn't exist
|
|
7366
|
+
|
|
6422
7367
|
let match;
|
|
6423
7368
|
while ((match = samplerRegex.exec(src)) !== null) {
|
|
6424
7369
|
const [_, group, binding, name, type] = match;
|
|
@@ -6453,21 +7398,51 @@ function rendererWebGPU(p5, fn) {
|
|
|
6453
7398
|
samplerNode.textureSource = sampler;
|
|
6454
7399
|
}
|
|
6455
7400
|
}
|
|
7401
|
+
|
|
7402
|
+
// Parse storage buffers
|
|
7403
|
+
while ((match = storageRegex.exec(src)) !== null) {
|
|
7404
|
+
const [_, group, binding, accessMode, name] = match;
|
|
7405
|
+
const groupIndex = parseInt(group);
|
|
7406
|
+
const bindingIndex = parseInt(binding);
|
|
7407
|
+
|
|
7408
|
+
const key = `${groupIndex},${bindingIndex}`;
|
|
7409
|
+
const existing = storageBuffers[key];
|
|
7410
|
+
// If any stage uses read_write, the bind group layout must use read_write
|
|
7411
|
+
const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write')
|
|
7412
|
+
? 'read_write'
|
|
7413
|
+
: accessMode;
|
|
7414
|
+
|
|
7415
|
+
storageBuffers[key] = {
|
|
7416
|
+
visibility: (existing?.visibility || 0) | visibility,
|
|
7417
|
+
group: groupIndex,
|
|
7418
|
+
binding: bindingIndex,
|
|
7419
|
+
name,
|
|
7420
|
+
accessMode: finalAccessMode, // 'read' or 'read_write'
|
|
7421
|
+
isStorage: true,
|
|
7422
|
+
type: 'storage'
|
|
7423
|
+
};
|
|
7424
|
+
}
|
|
6456
7425
|
}
|
|
6457
|
-
|
|
7426
|
+
|
|
7427
|
+
// Store storage buffers on shader for later use
|
|
7428
|
+
shader._storageBuffers = Object.values(storageBuffers);
|
|
7429
|
+
|
|
7430
|
+
return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)];
|
|
6458
7431
|
}
|
|
6459
7432
|
|
|
6460
|
-
getNextBindingIndex({ vert, frag }, group = 0) {
|
|
7433
|
+
getNextBindingIndex({ vert, frag, compute }, group = 0) {
|
|
6461
7434
|
// Get the highest binding index in the specified group and return the next available
|
|
6462
|
-
const
|
|
7435
|
+
const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g;
|
|
6463
7436
|
let maxBindingIndex = -1;
|
|
6464
7437
|
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
])
|
|
7438
|
+
const sources = [];
|
|
7439
|
+
if (vert) sources.push([vert, GPUShaderStage.VERTEX]);
|
|
7440
|
+
if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]);
|
|
7441
|
+
if (compute) sources.push([compute, GPUShaderStage.COMPUTE]);
|
|
7442
|
+
|
|
7443
|
+
for (const [src, visibility] of sources) {
|
|
6469
7444
|
let match;
|
|
6470
|
-
while ((match =
|
|
7445
|
+
while ((match = bindingRegex.exec(src)) !== null) {
|
|
6471
7446
|
const [_, groupIndex, bindingIndex] = match;
|
|
6472
7447
|
if (parseInt(groupIndex) === group) {
|
|
6473
7448
|
maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
|
|
@@ -6482,7 +7457,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
6482
7457
|
if (uniform.isSampler) {
|
|
6483
7458
|
uniform.texture =
|
|
6484
7459
|
data instanceof Texture ? data : this.getTexture(data);
|
|
6485
|
-
} else {
|
|
7460
|
+
} else if (!data?._isStorageBuffer) {
|
|
6486
7461
|
uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
|
|
6487
7462
|
}
|
|
6488
7463
|
shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
|
|
@@ -6589,7 +7564,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
6589
7564
|
rgb += components.emissive;
|
|
6590
7565
|
return vec4<f32>(rgb, components.opacity);
|
|
6591
7566
|
}`,
|
|
6592
|
-
"vec4f getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
7567
|
+
"vec4f getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
6593
7568
|
"void afterFragment": "() {}",
|
|
6594
7569
|
},
|
|
6595
7570
|
}
|
|
@@ -6614,7 +7589,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
6614
7589
|
},
|
|
6615
7590
|
fragment: {
|
|
6616
7591
|
"void beforeFragment": "() {}",
|
|
6617
|
-
"vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
7592
|
+
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
6618
7593
|
"void afterFragment": "() {}",
|
|
6619
7594
|
},
|
|
6620
7595
|
}
|
|
@@ -6640,7 +7615,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
6640
7615
|
fragment: {
|
|
6641
7616
|
"void beforeFragment": "() {}",
|
|
6642
7617
|
"Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
|
|
6643
|
-
"vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
7618
|
+
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
6644
7619
|
"bool shouldDiscard": "(outside: bool) { return outside; };",
|
|
6645
7620
|
"void afterFragment": "() {}",
|
|
6646
7621
|
},
|
|
@@ -6799,11 +7774,87 @@ function rendererWebGPU(p5, fn) {
|
|
|
6799
7774
|
}
|
|
6800
7775
|
);
|
|
6801
7776
|
|
|
6802
|
-
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment)\s*)?fn main[^{]+\{)/);
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
7777
|
+
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/);
|
|
7778
|
+
|
|
7779
|
+
const getBuiltinParamName = (mainSrc, builtinName) => {
|
|
7780
|
+
const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc);
|
|
7781
|
+
return match ? match[1] : null;
|
|
7782
|
+
};
|
|
7783
|
+
|
|
7784
|
+
const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => {
|
|
7785
|
+
const existingName = getBuiltinParamName(mainSrc, builtinName);
|
|
7786
|
+
if (existingName) {
|
|
7787
|
+
return { mainSrc, argName: existingName };
|
|
7788
|
+
}
|
|
7789
|
+
|
|
7790
|
+
const hasParams = /\(\s*\S/.test(mainSrc);
|
|
7791
|
+
const injectedMain = mainSrc.replace(
|
|
7792
|
+
/\)\s*(->|\{)/,
|
|
7793
|
+
`${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1`
|
|
7794
|
+
);
|
|
7795
|
+
|
|
7796
|
+
return { mainSrc: injectedMain, argName: fallbackName };
|
|
7797
|
+
};
|
|
7798
|
+
|
|
7799
|
+
const getMainStructParameter = (mainSrc) => {
|
|
7800
|
+
const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc);
|
|
7801
|
+
if (!match) return null;
|
|
7802
|
+
return { inputName: match[1], structName: match[2] };
|
|
7803
|
+
};
|
|
7804
|
+
|
|
7805
|
+
const getStructBuiltinFieldName = (structName, builtinName) => {
|
|
7806
|
+
const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain);
|
|
7807
|
+
if (!structMatch) return null;
|
|
7808
|
+
const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]);
|
|
7809
|
+
return fieldMatch ? fieldMatch[1] : null;
|
|
7810
|
+
};
|
|
7811
|
+
|
|
7812
|
+
const appendHookParams = (params, additionalParams) => {
|
|
7813
|
+
if (additionalParams.length === 0) return params;
|
|
7814
|
+
const hasParams = !/^\(\s*\)$/.test(params);
|
|
7815
|
+
return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`;
|
|
7816
|
+
};
|
|
7817
|
+
|
|
7818
|
+
let hookExtraParams = [];
|
|
7819
|
+
let hookExtraArgs = [];
|
|
7820
|
+
|
|
7821
|
+
if (shaderType === 'vertex') {
|
|
7822
|
+
const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32');
|
|
7823
|
+
main = ensuredInstance.mainSrc;
|
|
7824
|
+
|
|
7825
|
+
const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32');
|
|
7826
|
+
main = ensuredVertex.mainSrc;
|
|
7827
|
+
|
|
7828
|
+
hookExtraParams = ['instanceID: u32', '_p5VertexId: u32'];
|
|
7829
|
+
hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName];
|
|
7830
|
+
} else if (shaderType === 'fragment') {
|
|
7831
|
+
const directPositionArg = getBuiltinParamName(main, 'position');
|
|
7832
|
+
let fragmentPositionArg = directPositionArg;
|
|
7833
|
+
|
|
7834
|
+
if (!fragmentPositionArg) {
|
|
7835
|
+
const mainStructParam = getMainStructParameter(main);
|
|
7836
|
+
if (mainStructParam) {
|
|
7837
|
+
const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position');
|
|
7838
|
+
if (positionField) {
|
|
7839
|
+
fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`;
|
|
7840
|
+
}
|
|
7841
|
+
}
|
|
6806
7842
|
}
|
|
7843
|
+
|
|
7844
|
+
if (!fragmentPositionArg) {
|
|
7845
|
+
const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4<f32>');
|
|
7846
|
+
main = ensuredPosition.mainSrc;
|
|
7847
|
+
fragmentPositionArg = ensuredPosition.argName;
|
|
7848
|
+
}
|
|
7849
|
+
|
|
7850
|
+
hookExtraParams = ['_p5FragPos: vec4<f32>'];
|
|
7851
|
+
hookExtraArgs = [fragmentPositionArg];
|
|
7852
|
+
} else if (shaderType === 'compute') {
|
|
7853
|
+
const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3<u32>');
|
|
7854
|
+
main = ensuredGlobalId.mainSrc;
|
|
7855
|
+
|
|
7856
|
+
hookExtraParams = ['_p5GlobalId: vec3<u32>'];
|
|
7857
|
+
hookExtraArgs = [ensuredGlobalId.argName];
|
|
6807
7858
|
}
|
|
6808
7859
|
|
|
6809
7860
|
// Inject hook uniforms as a separate struct at a new binding
|
|
@@ -6823,6 +7874,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
6823
7874
|
const nextBinding = this.getNextBindingIndex({
|
|
6824
7875
|
vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
|
|
6825
7876
|
frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
|
|
7877
|
+
compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc,
|
|
6826
7878
|
}, 0);
|
|
6827
7879
|
|
|
6828
7880
|
// Create HookUniforms struct and binding
|
|
@@ -6833,8 +7885,14 @@ ${hookUniformFields}}
|
|
|
6833
7885
|
|
|
6834
7886
|
@group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
|
|
6835
7887
|
`;
|
|
6836
|
-
// Insert before the first @group binding
|
|
6837
|
-
|
|
7888
|
+
// Insert before the first @group binding, or at the end if there are none
|
|
7889
|
+
const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
|
|
7890
|
+
if (replaced === preMain) {
|
|
7891
|
+
// No @group bindings found in base shader, append to preMain
|
|
7892
|
+
preMain = preMain + '\n' + hookUniformsDecl;
|
|
7893
|
+
} else {
|
|
7894
|
+
preMain = replaced;
|
|
7895
|
+
}
|
|
6838
7896
|
}
|
|
6839
7897
|
|
|
6840
7898
|
// Handle varying variables by injecting them into VertexOutput and FragmentInput structs
|
|
@@ -6900,10 +7958,9 @@ ${hookUniformFields}}
|
|
|
6900
7958
|
initStatements += ` ${varName} = INPUT_VAR.${varName};\n`;
|
|
6901
7959
|
}
|
|
6902
7960
|
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
const inputVarName = inputMatch[1];
|
|
7961
|
+
const mainStructParam = getMainStructParameter(main);
|
|
7962
|
+
if (mainStructParam) {
|
|
7963
|
+
const inputVarName = mainStructParam.inputName;
|
|
6907
7964
|
initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName);
|
|
6908
7965
|
// Insert after the main function parameter but before any other code (anchored to start)
|
|
6909
7966
|
postMain = initStatements + postMain;
|
|
@@ -6911,12 +7968,56 @@ ${hookUniformFields}}
|
|
|
6911
7968
|
}
|
|
6912
7969
|
}
|
|
6913
7970
|
|
|
7971
|
+
// Handle instanceID varying for fragment access
|
|
7972
|
+
if (shader.hooks.instanceIDVarying) {
|
|
7973
|
+
const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying;
|
|
7974
|
+
const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType);
|
|
7975
|
+
const interpAttr = interpolation ? ` @interpolate(${interpolation})` : '';
|
|
7976
|
+
const [varName, varType] = declaration.split(':').map(s => s.trim());
|
|
7977
|
+
const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`;
|
|
7978
|
+
|
|
7979
|
+
if (shaderType === 'vertex') {
|
|
7980
|
+
// Inject into VertexOutput struct
|
|
7981
|
+
preMain = preMain.replace(
|
|
7982
|
+
/struct\s+VertexOutput\s+\{([^}]*)\}/,
|
|
7983
|
+
(match, body) => `struct VertexOutput {${body}\n${structMember}}`
|
|
7984
|
+
);
|
|
7985
|
+
// Add private global
|
|
7986
|
+
preMain += `var<private> ${declaration};\n`;
|
|
7987
|
+
// Assign from built-in instanceID at start of main()
|
|
7988
|
+
postMain = `\n ${varName} = ${source};\n` + postMain;
|
|
7989
|
+
// Copy to output struct before return
|
|
7990
|
+
const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
|
|
7991
|
+
if (returnMatch) {
|
|
7992
|
+
const outputVarName = returnMatch[1];
|
|
7993
|
+
postMain = postMain.replace(
|
|
7994
|
+
/(return\s+\w+\s*;)/g,
|
|
7995
|
+
`${outputVarName}.${varName} = ${varName};\n $1`
|
|
7996
|
+
);
|
|
7997
|
+
}
|
|
7998
|
+
} else if (shaderType === 'fragment') {
|
|
7999
|
+
// Inject into FragmentInput struct
|
|
8000
|
+
preMain = preMain.replace(
|
|
8001
|
+
/struct\s+FragmentInput\s+\{([^}]*)\}/,
|
|
8002
|
+
(match, body) => `struct FragmentInput {${body}\n${structMember}}`
|
|
8003
|
+
);
|
|
8004
|
+
// Add private global
|
|
8005
|
+
preMain += `var<private> ${declaration};\n`;
|
|
8006
|
+
// Initialize from input struct at start of main()
|
|
8007
|
+
const mainStructParam = getMainStructParameter(main);
|
|
8008
|
+
if (mainStructParam) {
|
|
8009
|
+
const inputVarName = mainStructParam.inputName;
|
|
8010
|
+
postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain;
|
|
8011
|
+
}
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
8014
|
+
|
|
6914
8015
|
let hooks = '';
|
|
6915
8016
|
let defines = '';
|
|
6916
8017
|
if (shader.hooks.declarations) {
|
|
6917
8018
|
hooks += shader.hooks.declarations + '\n';
|
|
6918
8019
|
}
|
|
6919
|
-
if (shader.hooks[shaderType].declarations) {
|
|
8020
|
+
if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) {
|
|
6920
8021
|
hooks += shader.hooks[shaderType].declarations + '\n';
|
|
6921
8022
|
}
|
|
6922
8023
|
for (const hookDef in shader.hooks.helpers) {
|
|
@@ -6940,11 +8041,7 @@ ${hookUniformFields}}
|
|
|
6940
8041
|
|
|
6941
8042
|
let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]);
|
|
6942
8043
|
|
|
6943
|
-
|
|
6944
|
-
// Splice the instance ID in as a final parameter to every WGSL hook function
|
|
6945
|
-
let hasParams = !!params.match(/^\(\s*\S+.*\)$/);
|
|
6946
|
-
params = params.slice(0, -1) + (hasParams ? ', ' : '') + 'instanceID: u32)';
|
|
6947
|
-
}
|
|
8044
|
+
params = appendHookParams(params, hookExtraParams);
|
|
6948
8045
|
|
|
6949
8046
|
if (hookType === 'void') {
|
|
6950
8047
|
hooks += `fn HOOK_${hookName}${params}${body}\n`;
|
|
@@ -6953,40 +8050,45 @@ ${hookUniformFields}}
|
|
|
6953
8050
|
}
|
|
6954
8051
|
}
|
|
6955
8052
|
|
|
6956
|
-
//
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
if (nesting === 0) {
|
|
6978
|
-
break;
|
|
6979
|
-
}
|
|
8053
|
+
// Pass stage-specific builtins from main to each hook call.
|
|
8054
|
+
// Collect ALL HOOK_ calls (including nested ones) then insert
|
|
8055
|
+
// extra args from right to left so position shifts don't
|
|
8056
|
+
// invalidate earlier insertion points.
|
|
8057
|
+
if (hookExtraArgs.length > 0) {
|
|
8058
|
+
const addHookArgs = (src) => {
|
|
8059
|
+
const insertions = [];
|
|
8060
|
+
let searchIdx = 0;
|
|
8061
|
+
let m;
|
|
8062
|
+
while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) {
|
|
8063
|
+
const openParen = searchIdx + m.index + m[0].length - 1;
|
|
8064
|
+
let pos = openParen + 1;
|
|
8065
|
+
let nesting = 1;
|
|
8066
|
+
let hasParams = false;
|
|
8067
|
+
while (pos < src.length && nesting > 0) {
|
|
8068
|
+
if (src[pos] === '(') nesting++;
|
|
8069
|
+
else if (src[pos] === ')') {
|
|
8070
|
+
nesting--;
|
|
8071
|
+
if (nesting === 0) break;
|
|
8072
|
+
} else if (/\S/.test(src[pos])) {
|
|
8073
|
+
hasParams = true;
|
|
6980
8074
|
}
|
|
6981
|
-
|
|
6982
|
-
result = result.slice(0, idx-1) + insertion + result.slice(idx-1);
|
|
6983
|
-
idx += insertion.length;
|
|
8075
|
+
pos++;
|
|
6984
8076
|
}
|
|
6985
|
-
|
|
8077
|
+
insertions.push({ pos, hasParams });
|
|
8078
|
+
searchIdx = openParen + 1;
|
|
8079
|
+
}
|
|
8080
|
+
|
|
8081
|
+
insertions.sort((a, b) => b.pos - a.pos);
|
|
8082
|
+
|
|
8083
|
+
let result = src;
|
|
8084
|
+
for (const { pos, hasParams } of insertions) {
|
|
8085
|
+
const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', ');
|
|
8086
|
+
result = result.slice(0, pos) + insertion + result.slice(pos);
|
|
8087
|
+
}
|
|
6986
8088
|
return result;
|
|
6987
8089
|
};
|
|
6988
|
-
preMain =
|
|
6989
|
-
postMain =
|
|
8090
|
+
preMain = addHookArgs(preMain);
|
|
8091
|
+
postMain = addHookArgs(postMain);
|
|
6990
8092
|
}
|
|
6991
8093
|
|
|
6992
8094
|
return preMain + '\n' + defines + hooks + main + postMain;
|
|
@@ -7045,6 +8147,10 @@ ${hookUniformFields}}
|
|
|
7045
8147
|
body = shader.hooks.fragment[hookName];
|
|
7046
8148
|
fullSrc = shader._fragSrc;
|
|
7047
8149
|
}
|
|
8150
|
+
if (!body) {
|
|
8151
|
+
body = shader.hooks.compute[hookName];
|
|
8152
|
+
fullSrc = shader._computeSrc;
|
|
8153
|
+
}
|
|
7048
8154
|
if (!body) {
|
|
7049
8155
|
throw new Error(`Can't find hook ${hookName}!`);
|
|
7050
8156
|
}
|
|
@@ -7176,7 +8282,7 @@ ${hookUniformFields}}
|
|
|
7176
8282
|
}
|
|
7177
8283
|
|
|
7178
8284
|
defaultFramebufferAntialias() {
|
|
7179
|
-
return
|
|
8285
|
+
return this._pInst._webgpuAttributes?.antialias !== false;
|
|
7180
8286
|
}
|
|
7181
8287
|
|
|
7182
8288
|
supportsFramebufferAntialias() {
|
|
@@ -7369,6 +8475,267 @@ ${hookUniformFields}}
|
|
|
7369
8475
|
};
|
|
7370
8476
|
}
|
|
7371
8477
|
|
|
8478
|
+
// Maps a plain JS value to the WGSL type string that represents it in a struct.
|
|
8479
|
+
_jsValueToWgslType(value) {
|
|
8480
|
+
if (typeof value === 'number') return 'f32';
|
|
8481
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
8482
|
+
// copy of the Color/Vector classes
|
|
8483
|
+
if (value?.isVector) {
|
|
8484
|
+
if (value.dimensions === 2) return 'vec2f';
|
|
8485
|
+
if (value.dimensions === 3) return 'vec3f';
|
|
8486
|
+
if (value.dimensions === 4) return 'vec4f';
|
|
8487
|
+
throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`);
|
|
8488
|
+
}
|
|
8489
|
+
if (value?.isColor) {
|
|
8490
|
+
return 'vec4f';
|
|
8491
|
+
}
|
|
8492
|
+
if (Array.isArray(value)) {
|
|
8493
|
+
if (value.length === 2) return 'vec2f';
|
|
8494
|
+
if (value.length === 3) return 'vec3f';
|
|
8495
|
+
if (value.length === 4) return 'vec4f';
|
|
8496
|
+
throw new Error(`Unsupported array length ${value.length} for struct storage field`);
|
|
8497
|
+
}
|
|
8498
|
+
throw new Error(`Unsupported value type ${typeof value} for struct storage field`);
|
|
8499
|
+
}
|
|
8500
|
+
|
|
8501
|
+
// Infers a struct schema from the first element of a struct array.
|
|
8502
|
+
//
|
|
8503
|
+
// Returns { fields, stride, structBody } where:
|
|
8504
|
+
// fields: field has the _packField interface (baseType, size, offset, packInPlace) plus:
|
|
8505
|
+
// name: string - JS property name
|
|
8506
|
+
// dim: number - float component count, used when creating StrandsNodes
|
|
8507
|
+
// structBody: everything inside the { ... } of a WGSL struct definition
|
|
8508
|
+
// stride: how many bytes are reserved for this struct in the buffer
|
|
8509
|
+
_inferStructSchema(firstElement) {
|
|
8510
|
+
const entries = Object.entries(firstElement);
|
|
8511
|
+
|
|
8512
|
+
if (!p5.disableFriendlyErrors) {
|
|
8513
|
+
for (const [name, value] of entries) {
|
|
8514
|
+
if (
|
|
8515
|
+
value !== null &&
|
|
8516
|
+
typeof value === 'object' &&
|
|
8517
|
+
!Array.isArray(value) &&
|
|
8518
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
8519
|
+
// copy of the Color/Vector classes
|
|
8520
|
+
!value?.isVector &&
|
|
8521
|
+
!value?.isColor
|
|
8522
|
+
) {
|
|
8523
|
+
p5._friendlyError(
|
|
8524
|
+
`The "${name}" property in your storage data contains a nested object. ` +
|
|
8525
|
+
`Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`,
|
|
8526
|
+
'createStorage'
|
|
8527
|
+
);
|
|
8528
|
+
}
|
|
8529
|
+
}
|
|
8530
|
+
}
|
|
8531
|
+
|
|
8532
|
+
const fieldLines = entries.map(([name, value]) =>
|
|
8533
|
+
` ${name}: ${this._jsValueToWgslType(value)},`
|
|
8534
|
+
).join('\n');
|
|
8535
|
+
const structBody = `{\n${fieldLines}\n}`;
|
|
8536
|
+
const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp');
|
|
8537
|
+
|
|
8538
|
+
let maxEnd = 0;
|
|
8539
|
+
let maxAlign = 1;
|
|
8540
|
+
const fields = entries.map(([name, value]) => {
|
|
8541
|
+
const el = elements[name];
|
|
8542
|
+
maxEnd = Math.max(maxEnd, el.offsetEnd);
|
|
8543
|
+
// Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
|
|
8544
|
+
const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
|
|
8545
|
+
maxAlign = Math.max(maxAlign, align);
|
|
8546
|
+
// Track original JS type for reconstruction during readback
|
|
8547
|
+
const kind = value?.isVector ? 'vector'
|
|
8548
|
+
: value?.isColor ? 'color'
|
|
8549
|
+
: undefined;
|
|
8550
|
+
return {
|
|
8551
|
+
name,
|
|
8552
|
+
baseType: el.baseType,
|
|
8553
|
+
size: el.size,
|
|
8554
|
+
offset: el.offset,
|
|
8555
|
+
packInPlace: el.packInPlace ?? false,
|
|
8556
|
+
dim: el.size / 4,
|
|
8557
|
+
kind,
|
|
8558
|
+
};
|
|
8559
|
+
});
|
|
8560
|
+
|
|
8561
|
+
const stride = Math.ceil(maxEnd / maxAlign) * maxAlign;
|
|
8562
|
+
return { fields, stride, structBody };
|
|
8563
|
+
}
|
|
8564
|
+
|
|
8565
|
+
// Packs an array of plain objects into a Float32Array using the given struct schema.
|
|
8566
|
+
// Reuses _packField so layout rules match uniform packing exactly.
|
|
8567
|
+
_packStructArray(data, schema) {
|
|
8568
|
+
const { fields, stride } = schema;
|
|
8569
|
+
const totalBytes = Math.max(data.length * stride, 16);
|
|
8570
|
+
const alignedBytes = Math.ceil(totalBytes / 16) * 16;
|
|
8571
|
+
const buffer = new ArrayBuffer(alignedBytes);
|
|
8572
|
+
const floatView = new Float32Array(buffer);
|
|
8573
|
+
const dataView = new DataView(buffer);
|
|
8574
|
+
for (let i = 0; i < data.length; i++) {
|
|
8575
|
+
const item = data[i];
|
|
8576
|
+
const baseOffset = i * stride;
|
|
8577
|
+
for (const field of fields) {
|
|
8578
|
+
this._packField(field, item[field.name], floatView, dataView, baseOffset);
|
|
8579
|
+
}
|
|
8580
|
+
}
|
|
8581
|
+
return floatView;
|
|
8582
|
+
}
|
|
8583
|
+
|
|
8584
|
+
// Inverse of _packStructArray reads packed buffer back into plain JS objects
|
|
8585
|
+
// using the same schema layout - fields, stride and offsets
|
|
8586
|
+
_unpackStructArray(floatView, schema) {
|
|
8587
|
+
const { fields, stride } = schema;
|
|
8588
|
+
const dataView = new DataView(floatView.buffer);
|
|
8589
|
+
const count = Math.floor(floatView.byteLength / stride);
|
|
8590
|
+
const result = [];
|
|
8591
|
+
|
|
8592
|
+
for (let i = 0; i < count; i++) {
|
|
8593
|
+
const item = {};
|
|
8594
|
+
const baseOffset = i * stride;
|
|
8595
|
+
for (const field of fields) {
|
|
8596
|
+
const byteOffset = baseOffset + field.offset;
|
|
8597
|
+
const n = field.size / 4;
|
|
8598
|
+
|
|
8599
|
+
if (field.baseType === 'u32') {
|
|
8600
|
+
if (n === 1) {
|
|
8601
|
+
item[field.name] = dataView.getUint32(byteOffset, true);
|
|
8602
|
+
} else {
|
|
8603
|
+
item[field.name] = Array.from({ length: n }, (_, j) =>
|
|
8604
|
+
dataView.getUint32(byteOffset + j * 4, true)
|
|
8605
|
+
);
|
|
8606
|
+
}
|
|
8607
|
+
} else if (field.baseType === 'i32') {
|
|
8608
|
+
if (n === 1) {
|
|
8609
|
+
item[field.name] = dataView.getInt32(byteOffset, true);
|
|
8610
|
+
} else {
|
|
8611
|
+
item[field.name] = Array.from({ length: n }, (_, j) =>
|
|
8612
|
+
dataView.getInt32(byteOffset + j * 4, true)
|
|
8613
|
+
);
|
|
8614
|
+
}
|
|
8615
|
+
} else {
|
|
8616
|
+
const idx = byteOffset / 4;
|
|
8617
|
+
if (n === 1) {
|
|
8618
|
+
item[field.name] = floatView[idx];
|
|
8619
|
+
} else {
|
|
8620
|
+
const values = Array.from(floatView.slice(idx, idx + n));
|
|
8621
|
+
if (field.kind === 'vector') {
|
|
8622
|
+
item[field.name] = this._pInst.createVector(...values);
|
|
8623
|
+
} else if (field.kind === 'color') {
|
|
8624
|
+
// Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
|
|
8625
|
+
// Scale back to the current colorMode range
|
|
8626
|
+
const maxes = this.states.colorMaxes[this.states.colorMode];
|
|
8627
|
+
item[field.name] = this._pInst.color(
|
|
8628
|
+
values[0] * maxes[0], values[1] * maxes[1],
|
|
8629
|
+
values[2] * maxes[2], values[3] * maxes[3]
|
|
8630
|
+
);
|
|
8631
|
+
} else {
|
|
8632
|
+
item[field.name] = values;
|
|
8633
|
+
}
|
|
8634
|
+
}
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
8637
|
+
result.push(item);
|
|
8638
|
+
}
|
|
8639
|
+
|
|
8640
|
+
return result;
|
|
8641
|
+
}
|
|
8642
|
+
|
|
8643
|
+
createStorage(dataOrCount) {
|
|
8644
|
+
const device = this.device;
|
|
8645
|
+
|
|
8646
|
+
// Struct array: an array of plain objects
|
|
8647
|
+
if (Array.isArray(dataOrCount) && dataOrCount.length > 0 &&
|
|
8648
|
+
typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) {
|
|
8649
|
+
if (!p5.disableFriendlyErrors && dataOrCount.length > 1) {
|
|
8650
|
+
const firstKeys = Object.keys(dataOrCount[0]);
|
|
8651
|
+
let warned = false;
|
|
8652
|
+
for (let i = 1; i < dataOrCount.length; i++) {
|
|
8653
|
+
const el = dataOrCount[i];
|
|
8654
|
+
const elKeys = Object.keys(el);
|
|
8655
|
+
const sameKeys = firstKeys.length === elKeys.length &&
|
|
8656
|
+
firstKeys.every((k, j) => k === elKeys[j]);
|
|
8657
|
+
if (!sameKeys) {
|
|
8658
|
+
p5._friendlyError(
|
|
8659
|
+
`Element ${i} has different fields than element 0. ` +
|
|
8660
|
+
`All elements should have the same properties.`,
|
|
8661
|
+
'createStorage'
|
|
8662
|
+
);
|
|
8663
|
+
break;
|
|
8664
|
+
}
|
|
8665
|
+
for (const key of firstKeys) {
|
|
8666
|
+
const firstType = this._jsValueToWgslType(dataOrCount[0][key]);
|
|
8667
|
+
const elType = this._jsValueToWgslType(el[key]);
|
|
8668
|
+
if (firstType !== elType) {
|
|
8669
|
+
p5._friendlyError(
|
|
8670
|
+
`The "${key}" property of element ${i} has type ${elType} ` +
|
|
8671
|
+
`but element 0 has type ${firstType}. Proporties should have the same type across all elements.`,
|
|
8672
|
+
'createStorage'
|
|
8673
|
+
);
|
|
8674
|
+
warned = true;
|
|
8675
|
+
break;
|
|
8676
|
+
}
|
|
8677
|
+
}
|
|
8678
|
+
if (warned) break;
|
|
8679
|
+
}
|
|
8680
|
+
}
|
|
8681
|
+
const schema = this._inferStructSchema(dataOrCount[0]);
|
|
8682
|
+
const packed = this._packStructArray(dataOrCount, schema);
|
|
8683
|
+
const size = packed.byteLength;
|
|
8684
|
+
const buffer = device.createBuffer({
|
|
8685
|
+
size,
|
|
8686
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
8687
|
+
mappedAtCreation: true,
|
|
8688
|
+
});
|
|
8689
|
+
new Float32Array(buffer.getMappedRange()).set(packed);
|
|
8690
|
+
buffer.unmap();
|
|
8691
|
+
const storageBuffer = new StorageBuffer(buffer, size, this, schema);
|
|
8692
|
+
this._storageBuffers.add(storageBuffer);
|
|
8693
|
+
return storageBuffer;
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8696
|
+
// Determine buffer size and initial data
|
|
8697
|
+
let size, initialData;
|
|
8698
|
+
if (typeof dataOrCount === 'number') {
|
|
8699
|
+
// createStorage(count) - zero-initialized
|
|
8700
|
+
size = dataOrCount * 4; // floats are 4 bytes
|
|
8701
|
+
initialData = new Float32Array(dataOrCount);
|
|
8702
|
+
} else {
|
|
8703
|
+
// createStorage(array) - from data
|
|
8704
|
+
if (dataOrCount instanceof Float32Array) {
|
|
8705
|
+
initialData = dataOrCount;
|
|
8706
|
+
} else if (Array.isArray(dataOrCount)) {
|
|
8707
|
+
initialData = new Float32Array(dataOrCount);
|
|
8708
|
+
} else {
|
|
8709
|
+
throw new Error('createStorage expects a number or array/Float32Array');
|
|
8710
|
+
}
|
|
8711
|
+
size = initialData.byteLength;
|
|
8712
|
+
}
|
|
8713
|
+
|
|
8714
|
+
// Align to 16 bytes (WGSL storage buffer alignment requirement)
|
|
8715
|
+
size = Math.ceil(size / 16) * 16;
|
|
8716
|
+
|
|
8717
|
+
// Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage
|
|
8718
|
+
const buffer = device.createBuffer({
|
|
8719
|
+
size,
|
|
8720
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
8721
|
+
mappedAtCreation: initialData.length > 0
|
|
8722
|
+
});
|
|
8723
|
+
|
|
8724
|
+
// Write initial data if provided
|
|
8725
|
+
if (initialData.length > 0) {
|
|
8726
|
+
const mapping = new Float32Array(buffer.getMappedRange());
|
|
8727
|
+
mapping.set(initialData);
|
|
8728
|
+
buffer.unmap();
|
|
8729
|
+
}
|
|
8730
|
+
|
|
8731
|
+
const storageBuffer = new StorageBuffer(buffer, size, this);
|
|
8732
|
+
|
|
8733
|
+
// Track for cleanup
|
|
8734
|
+
this._storageBuffers.add(storageBuffer);
|
|
8735
|
+
|
|
8736
|
+
return storageBuffer;
|
|
8737
|
+
}
|
|
8738
|
+
|
|
7372
8739
|
_getWebGPUColorFormat(framebuffer) {
|
|
7373
8740
|
if (framebuffer.format === FLOAT) {
|
|
7374
8741
|
return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float';
|
|
@@ -7665,10 +9032,6 @@ ${hookUniformFields}}
|
|
|
7665
9032
|
return super.filter(...args);
|
|
7666
9033
|
}
|
|
7667
9034
|
|
|
7668
|
-
getNoiseShaderSnippet() {
|
|
7669
|
-
return noiseWGSL;
|
|
7670
|
-
}
|
|
7671
|
-
|
|
7672
9035
|
|
|
7673
9036
|
baseFilterShader() {
|
|
7674
9037
|
if (!this._baseFilterShader) {
|
|
@@ -7692,6 +9055,21 @@ ${hookUniformFields}}
|
|
|
7692
9055
|
return this._baseFilterShader;
|
|
7693
9056
|
}
|
|
7694
9057
|
|
|
9058
|
+
baseComputeShader() {
|
|
9059
|
+
if (!this._baseComputeShader) {
|
|
9060
|
+
this._baseComputeShader = new Shader(
|
|
9061
|
+
this,
|
|
9062
|
+
baseComputeShader,
|
|
9063
|
+
{
|
|
9064
|
+
compute: {
|
|
9065
|
+
'void iteration': '(index: vec3<i32>) {}',
|
|
9066
|
+
},
|
|
9067
|
+
}
|
|
9068
|
+
);
|
|
9069
|
+
}
|
|
9070
|
+
return this._baseComputeShader;
|
|
9071
|
+
}
|
|
9072
|
+
|
|
7695
9073
|
/*
|
|
7696
9074
|
* WebGPU-specific implementation of imageLight shader creation
|
|
7697
9075
|
*/
|
|
@@ -7791,6 +9169,69 @@ ${hookUniformFields}}
|
|
|
7791
9169
|
glDataType: dataType || 'uint8'
|
|
7792
9170
|
};
|
|
7793
9171
|
}
|
|
9172
|
+
|
|
9173
|
+
compute(shader, x, y = 1, z = 1) {
|
|
9174
|
+
if (shader.shaderType !== 'compute') {
|
|
9175
|
+
throw new Error('compute() can only be called with a compute shader');
|
|
9176
|
+
}
|
|
9177
|
+
|
|
9178
|
+
this._finishActiveRenderPass();
|
|
9179
|
+
|
|
9180
|
+
// Ensure shader is initialized and finalized
|
|
9181
|
+
if (!shader._compiled) {
|
|
9182
|
+
shader.init();
|
|
9183
|
+
}
|
|
9184
|
+
|
|
9185
|
+
// Set default uniforms
|
|
9186
|
+
shader.setDefaultUniforms();
|
|
9187
|
+
shader.setUniform('uTotalCount', [x, y, z]);
|
|
9188
|
+
|
|
9189
|
+
// Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup)
|
|
9190
|
+
const WORKGROUP_SIZE_X = 8;
|
|
9191
|
+
const WORKGROUP_SIZE_Y = 8;
|
|
9192
|
+
const WORKGROUP_SIZE_Z = 1;
|
|
9193
|
+
|
|
9194
|
+
// auto spreading: if any dimension is too large or for performance optimization,
|
|
9195
|
+
// spread total iteration count across dimensions
|
|
9196
|
+
const totalIterations = x * y * z;
|
|
9197
|
+
const MAX_THREADS_PER_DIM = 65535 * 8;
|
|
9198
|
+
|
|
9199
|
+
let px = x;
|
|
9200
|
+
let py = y;
|
|
9201
|
+
let pz = z;
|
|
9202
|
+
|
|
9203
|
+
// we spread if we exceed GPU limits OR if it involves a large 1D dispatch
|
|
9204
|
+
const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM;
|
|
9205
|
+
const isLarge1D = totalIterations > 1024 && y === 1 && z === 1;
|
|
9206
|
+
|
|
9207
|
+
if (exceedsLimits || isLarge1D) {
|
|
9208
|
+
// Always use 2D square spreading (√N × √N).
|
|
9209
|
+
// Benchmarks showed 2D square equals or outperforms 3D cube at every
|
|
9210
|
+
// scale tested, with simpler index reconstruction in the shader.
|
|
9211
|
+
px = Math.ceil(Math.sqrt(totalIterations));
|
|
9212
|
+
py = Math.ceil(totalIterations / px);
|
|
9213
|
+
pz = 1;
|
|
9214
|
+
}
|
|
9215
|
+
|
|
9216
|
+
shader.setUniform('uPhysicalCount', [px, py, pz]);
|
|
9217
|
+
|
|
9218
|
+
const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X);
|
|
9219
|
+
const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y);
|
|
9220
|
+
const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z);
|
|
9221
|
+
|
|
9222
|
+
const commandEncoder = this.device.createCommandEncoder();
|
|
9223
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
9224
|
+
this.setupShaderBindGroups(shader, passEncoder, {
|
|
9225
|
+
compute: true,
|
|
9226
|
+
workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z],
|
|
9227
|
+
});
|
|
9228
|
+
|
|
9229
|
+
// Dispatch compute workgroups
|
|
9230
|
+
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
|
|
9231
|
+
|
|
9232
|
+
passEncoder.end();
|
|
9233
|
+
this.device.queue.submit([commandEncoder.finish()]);
|
|
9234
|
+
}
|
|
7794
9235
|
}
|
|
7795
9236
|
|
|
7796
9237
|
p5.RendererWebGPU = RendererWebGPU;
|
|
@@ -7801,6 +9242,7 @@ ${hookUniformFields}}
|
|
|
7801
9242
|
fn.setAttributes = async function (key, value) {
|
|
7802
9243
|
return this._renderer._setAttributes(key, value);
|
|
7803
9244
|
};
|
|
9245
|
+
|
|
7804
9246
|
}
|
|
7805
9247
|
|
|
7806
9248
|
if (typeof p5 !== "undefined") {
|