glslh 1.0.0 → 1.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ ## 📋 `glslh` Changelog
2
+
3
+ ### **v1.0.1** (Latest)
4
+
5
+ - **Strict File Extension Whitelisting**: Added validation checks inside `GLSLHParser` to restrict `#include` directives exclusively to valid shader extensions (`.glsl`, `.glslh`, `.frag`, `.vert`, `.frag.glsl`, `.vert.glsl`), preventing security risks and unauthorized inclusions of non-shader files (e.g., `.py`, `.cpp`, `.txt`).
6
+ - **Compound Header Support**: Enhanced path checking to natively support complex shader extension combinations like `.frag.glslh` and `.vert.glslh`.
7
+ - **Robust Error Messaging**: Improved failure handling and descriptive console warnings/errors when file resolution or extension validation fails.
8
+
9
+ ---
10
+
11
+ ### **v1.0.0**
12
+
13
+ - **Recursive C-Style `#include` Preprocessor**: Introduced core parsing logic enabling `.glslh` and `.glsl` files to modularly include other shader headers recursively with built-in circular dependency protection.
14
+ - **Dynamic JavaScript Math Constant Injection**: Added automatic replacement of JS math expressions (such as `Math.PI` and `Math.TAU`) directly into clean GLSL numeric floats.
15
+ - **Metadata Uniforms Block (`@uniforms`)**: Implemented support for JSON-parsed `@uniforms` comment blocks to automatically generate and format Three.js uniform maps.
16
+ - **Zero-Config DOM Canvas Integration (`GLSLHDomCanvas`)**: Released `autoInitGlslCanvases()` to instantly bind canvas elements to Three.js WebGL renderers with automatic shader loading and animation loops.
package/README.md CHANGED
@@ -13,22 +13,23 @@
13
13
  `glslh` establishes a clean pipeline separating CPU-side orchestration from GPU-side parallel rendering:
14
14
 
15
15
  ```
16
- .glslh (Self-Contained Header)
17
- ┌─────────────────────────────┐
18
- │ /* @uniforms { ... } */ │ ──► [CPU] Auto-linked JS-to-GLSL Uniforms
19
- │ #include "utils/noise.glsl"│ ──► [CPU] Recursive File & Dependency Resolution
20
- │ Math.PI macro injection │ ──► [CPU] JIT Constant Evaluator
21
- └──────────────┬──────────────┘
22
- ▼
23
- [ glslh Preprocessor ]
24
- ▼
25
- ┌─────────────────────────────┐
26
- │ Final Pure GLSL String │ ──► [GPU] Compiled & Executed via WebGL
27
- └─────────────────────────────┘
16
+
17
+ .glslh (Self-Contained Header)
18
+ ┌─────────────────────────────┐
19
+ │ /* @uniforms { ... } */ │ ──► [CPU] Auto-linked JS-to-GLSL Uniforms
20
+ │ #include "utils/noise.glsl"│ ──► [CPU] Recursive File & Dependency Resolution
21
+ │ Math.PI macro injection │ ──► [CPU] JIT Constant & Expression Evaluator
22
+ └──────────────┬──────────────┘
23
+ ▼
24
+ [ glslh Preprocessor ]
25
+ ▼
26
+ ┌─────────────────────────────┐
27
+ │ Final Pure GLSL String │ ──► [GPU] Compiled & Executed via WebGL
28
+ └─────────────────────────────┘
28
29
 
29
30
  ```
30
31
 
31
- - **CPU-Side (JavaScript):** File fetching, `#include` stitching, uniform metadata parsing, constant macro evaluation (`Math.PI`, `Math.TAU`), and real-time animation loops (`requestAnimationFrame`).
32
+ - **CPU-Side (JavaScript):** File fetching, `#include` stitching, uniform metadata parsing, constant macro evaluation (`Math.PI`, `Math.TAU`), function collision checking, error line mapping, and real-time derived uniform animation loops (`requestAnimationFrame`).
32
33
  - **GPU-Side (WebGL / GLSL):** Pure parallel rasterization, vertex positioning, and fragment color blending executing simultaneously across graphics hardware cores.
33
34
 
34
35
  ---
@@ -37,8 +38,10 @@
37
38
 
38
39
  - **Clear CPU/GPU Separation:** All orchestration, math constant calculations (`Math.PI`), and uniform state updates run efficiently on the **CPU**, while heavy rasterization and pixel shading run natively in parallel on the **GPU**.
39
40
  - **Zero Runtime Performance Cost:** Because modular files are stitched and compiled once during initialization, your shaders execute at full native GPU speed with zero runtime overhead.
40
- - **C-Style `#include` Directives:** Split massive shaders into modular files with full circular dependency protection.
41
- - **Self-Contained Metadata Blocks:** Define uniform data types and default values right at the top of your shader file (`@uniforms`).
41
+ - **C-Style `#include` Directives:** Split massive shaders into modular files with full circular dependency protection and strict extension validation.
42
+ - **Duplicate Function Collision Detection:** Automatically scans included modules and warns you if multiple files define conflicting function signatures (`float hash()`) before WebGL compilation fails.
43
+ - **Error Line Mapping:** Instantly translates monolithic WebGL compilation error line numbers back to their exact source file and line number (`[shape_utils.glsl:14]`).
44
+ - **Self-Contained Metadata Blocks:** Define uniform data types, default values, and derived per-frame formulas right at the top of your shader file (`@uniforms`).
42
45
  - **Direct JavaScript Math Injection:** Use native JS constants like `Math.PI` or `Math.TAU` directly in your GLSL code; the preprocessor evaluates them before GPU compilation.
43
46
  - **Framework Agnostic:** Seamlessly output standard Three.js `ShaderMaterial` objects ready for Vanilla JS, Three.js, or React Three Fiber.
44
47
  - **HTML Declarative Support:** Spin up shaders instantly using HTML data attributes (`data-fragment-url="..."`).
@@ -53,7 +56,8 @@ The metadata block is an optional JSON declaration placed at the very top of you
53
56
  /* @uniforms
54
57
  {
55
58
  "u_intensity": { "type": "f", "value": 1.0 },
56
- "u_color": { "type": "c", "value": [1.0, 0.4, 0.0] }
59
+ "u_color": { "type": "c", "value": [1.0, 0.4, 0.0] },
60
+ "u_wave": { "type": "f", "expr": "sin(u_time * 2.0) * 0.5" }
57
61
  }
58
62
  */
59
63
 
@@ -61,14 +65,28 @@ The metadata block is an optional JSON declaration placed at the very top of you
61
65
 
62
66
  ### Why use it?
63
67
 
64
- In standard WebGL and Three.js, wiring JavaScript variables to shader uniforms requires tedious boilerplate. The `@uniforms` block decentralizes this configuration, letting your shader file declare its own parameters so `glslh` can auto-bind them into Three.js uniform structures instantly.
68
+ In standard WebGL and Three.js, wiring JavaScript variables to shader uniforms requires tedious boilerplate. The `@uniforms` block decentralizes this configuration, letting your shader file declare its own parameters and derived per-frame expressions so `glslh` can auto-bind and update them instantly.
65
69
 
66
70
  ### What if you forget it?
67
71
 
68
72
  **Nothing breaks.** The parser is completely fault-tolerant. If the comment block is missing, the parser simply returns an empty uniform object (`{}`), and your shader compiles and renders successfully using standard built-in uniforms (like `u_time` and `u_resolution`).
69
73
 
70
- - **When to Use:** Use it when your shader needs custom adjustable parameters, colors, or default numerical inputs managed from JavaScript.
71
- - **When to Omit:** Omit it when your shader is purely procedural, self-contained, or relies solely on built-in time and coordinate attributes.
74
+ ---
75
+
76
+ ## 📂 Project Structure
77
+
78
+ ```text
79
+ package/
80
+ ├── package.json
81
+ ├── README.md
82
+ └── src/
83
+ ├── parser.js # C-style #include preprocessor, path resolution, collision check & error mapper
84
+ ├── loader.js # Metadata JSON parser & Three.js uniform binder
85
+ ├── dom-canvas.js # HTML data-attribute and canvas auto-loader wrapper
86
+ ├── expr.js # Safe, CSP-compliant AST-based JIT expression evaluator
87
+ └── derived.js # Per-frame derived uniform state manager
88
+
89
+ ```
72
90
 
73
91
  ---
74
92
 
@@ -80,29 +98,24 @@ npm install glslh
80
98
 
81
99
  ### 1. Create a `.glslh` Shader File (`./shaders/header.glslh`)
82
100
 
83
- Refer to the complete working example setup here: [GLSLH](./test/shaders/header.glslh)
101
+ Refer to the complete working example setup here: [GLSLH](https://www.google.com/search?q=./test/shaders/header.glslh)
84
102
 
85
103
  ### 2. Run Declaratively in HTML (`index.html`)
86
104
 
87
- Refer to the complete working example setup here: [index.html](./test/index.html)
105
+ Refer to the complete working example setup here: [index.html](https://www.google.com/search?q=./test/index.html)
88
106
 
89
107
  ---
90
108
 
91
- ## 📂 Project Structure
109
+ ## ⚡ VS Code Extension (Live Preview)
92
110
 
93
- ```text
94
- package/
95
- ├── package.json
96
- ├── README.md
97
- └── src/
98
- ├── parser.js # The C-style #include preprocessor & Math.PI injector
99
- ├── loader.js # Metadata JSON parser & Three.js uniform binder
100
- └── dom-canvas.js # HTML data-attribute and canvas auto-loader wrapper
111
+ Want instant syntax highlighting, color pickers, and a live WebGL preview pane right inside your editor? Install the official **GLSLH Previewer** extension from the marketplace:
101
112
 
102
- ```
113
+ - **Live WebGL Preview:** Inspect shaders side-by-side with automatic hot-reloading on save.
114
+ - **Native Color Pickers:** Click color definitions or uniform arrays in `@uniforms` or `vec3()` blocks to adjust values visually.
115
+ - **Multi-file Resolution:** Seamlessly traces `#include` dependencies across your project directory.
103
116
 
104
117
  ---
105
118
 
106
119
  ## 📜 License
107
120
 
108
- Distributed under the **MIT License**. See `LICENSE` for more information.
121
+ Distributed under the [**MIT License**](../LICENSE). See `LICENSE` for more information.
@@ -1,10 +1,10 @@
1
- <?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24">
2
- <desc>Created with SVGMaker Editor 3.5.0</desc>
3
- <defs>
4
- </defs>
5
- <path style="stroke: rgb(255,255,255); stroke-opacity: 0; fill: rgb(171,71,188); fill-rule: evenodd" stroke-linecap="round" d="M1.0408 8.824C2.6084 7.0349 5.4668 6.8126 7.591 8.3143 10.9769 10.6363 12.6966 10.6349 15.6607 8.2989 17.5028 6.7643 20.3984 6.9394 22.2957 8.7001 24.193 10.4608 24.448 13.2092 22.8799 14.998 21.3119 16.7868 18.4532 17.0087 16.3293 15.5065 12.9424 13.1865 11.2237 13.1858 8.2607 15.5225 6.4187 17.057 3.5233 16.8821 1.6259 15.1216-0.2714 13.3613-0.5268 10.613 1.0408 8.824Z"/>
6
- <path style="stroke: rgb(255,255,255); stroke-opacity: 0; fill: rgb(171,71,188); fill-rule: evenodd" stroke-linecap="round" d="M19.8155 7.7205C21.8744 7.7205 23.5435 6.092 23.5435 4.083 23.5435 2.074 21.8744 0.4455 19.8155 0.4455 17.7566 0.4455 16.0876 2.074 16.0876 4.083 16.0876 6.092 17.7566 7.7205 19.8155 7.7205Z"/>
7
- <path style="stroke: rgb(255,255,255); stroke-opacity: 0; fill: rgb(171,71,188); fill-rule: evenodd" stroke-linecap="round" d="M19.8155 23.5546C21.8744 23.5546 23.5435 21.926 23.5435 19.917 23.5435 17.908 21.8744 16.2795 19.8155 16.2795 17.7566 16.2795 16.0876 17.908 16.0876 19.917 16.0876 21.926 17.7566 23.5546 19.8155 23.5546Z"/>
8
- <path style="stroke: rgb(255,255,255); stroke-opacity: 0; fill: rgb(171,71,188); fill-rule: evenodd" stroke-linecap="round" d="M4.1845 7.7205C6.2434 7.7205 7.9124 6.092 7.9124 4.083 7.9124 2.074 6.2434 0.4454 4.1845 0.4454 2.1256 0.4454 0.4565 2.074 0.4565 4.083 0.4565 6.092 2.1256 7.7205 4.1845 7.7205Z"/>
9
- <path style="stroke: rgb(255,255,255); stroke-opacity: 0; fill: rgb(171,71,188); fill-rule: evenodd" stroke-linecap="round" d="M4.1845 23.5546C6.2434 23.5546 7.9124 21.926 7.9124 19.917 7.9124 17.908 6.2434 16.2795 4.1845 16.2795 2.1256 16.2795 0.4565 17.908 0.4565 19.917 0.4565 21.926 2.1256 23.5546 4.1845 23.5546Z"/>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ab47bc">
2
+ <defs>
3
+ <circle id="circle" cx="4.184" cy="4.083" r="3.628" />
4
+ </defs>
5
+ <path fill-rule="evenodd" d="M1.041 8.824C2.608 7.035 5.467 6.813 7.591 8.314c3.386 2.322 5.106 2.321 8.07-0.015 1.842-1.535 4.738-1.36 6.635 0.401 1.897 1.761 2.152 4.51 0.584 6.298-1.568 1.789-4.427 2.011-6.551 0.509-3.387-2.32-5.105-2.321-8.068 0.016-1.842 1.534-4.737 1.359-6.635-0.402-1.897-1.761-2.152-4.509-0.585-6.297z" />
6
+ <use href="#circle" x="15.631" y="-.5" />
7
+ <use href="#circle" x="15.631" y="16.3" />
8
+ <use href="#circle" y="-0.5" />
9
+ <use href="#circle" y="16.3"/>
10
10
  </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glslh",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A lightweight GLSL preprocessor and runtime bridge for modular shader development.",
5
5
  "type": "module",
6
6
  "main": "./src/dom-canvas.js",
@@ -24,7 +24,8 @@
24
24
  "threejs",
25
25
  "preprocessor",
26
26
  "webgl",
27
- "graphics"
27
+ "graphics",
28
+ "glslh"
28
29
  ],
29
30
  "author": "Sudhanshu Ambastha",
30
31
  "license": "MIT",
package/src/derived.js ADDED
@@ -0,0 +1,51 @@
1
+ import { compileExpr } from "./expr.js";
2
+
3
+ export function buildDerived(defs) {
4
+ const items = Object.entries(defs)
5
+ .filter(([, d]) => typeof d.expr === "string")
6
+ .map(([name, d]) => ({ name, ...compileExpr(d.expr) }));
7
+
8
+ let first = true;
9
+
10
+ return function update(
11
+ uniforms,
12
+ { time, delta, width, height, mouseX = 0, mouseY = 0 },
13
+ ) {
14
+ const ctx = {
15
+ time,
16
+ delta,
17
+ width,
18
+ height,
19
+ aspect: width / height,
20
+ mouseX,
21
+ mouseY,
22
+ };
23
+
24
+ for (const [n, u] of Object.entries(uniforms)) {
25
+ if (typeof u.value === "number") {
26
+ ctx[n] = u.value;
27
+ }
28
+ }
29
+
30
+ for (const it of items) {
31
+ if (!first && it.deps.length === 0) continue;
32
+
33
+ const v = it.run(ctx);
34
+ const u = uniforms[it.name];
35
+
36
+ if (!u) continue;
37
+
38
+ if (Array.isArray(v) && typeof u.value.set === "function") {
39
+ u.value.set(...v);
40
+ } else {
41
+ u.value = v;
42
+ }
43
+
44
+ if (typeof v === "number") {
45
+ ctx[it.name] = v;
46
+ }
47
+ }
48
+
49
+ first = false;
50
+ };
51
+ }
package/src/dom-canvas.js CHANGED
@@ -5,20 +5,21 @@ export class GLSLHDomCanvas {
5
5
  constructor(canvasElement) {
6
6
  this.canvas = canvasElement;
7
7
  this.loader = new GLSLHLoader();
8
+ this.width = Number.parseInt(this.canvas.getAttribute("width"), 10) || 600;
9
+ this.height =
10
+ Number.parseInt(this.canvas.getAttribute("height"), 10) || 400;
8
11
  this.init();
9
12
  }
10
13
 
11
14
  async init() {
12
15
  const fragUrl = this.canvas.getAttribute("data-fragment-url");
13
- const width = parseInt(this.canvas.getAttribute("width")) || 600;
14
- const height = parseInt(this.canvas.getAttribute("height")) || 400;
15
16
 
16
17
  console.log("GLSLH: Initializing WebGL Renderer...");
17
18
  this.renderer = new THREE.WebGLRenderer({
18
19
  canvas: this.canvas,
19
20
  antialias: true,
20
21
  });
21
- this.renderer.setSize(width, height, false);
22
+ this.renderer.setSize(this.width, this.height, false);
22
23
 
23
24
  this.scene = new THREE.Scene();
24
25
  this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
@@ -30,21 +31,18 @@ export class GLSLHDomCanvas {
30
31
  `;
31
32
 
32
33
  let fragmentShader = "";
33
- let uniforms = {
34
- u_time: { value: 0.0 },
35
- u_resolution: { value: new THREE.Vector2(width, height) },
36
- };
34
+ let uniforms = {};
35
+ let shaderModule = null;
37
36
 
38
37
  try {
39
38
  console.log(`GLSLH: Fetching and parsing shader module -> ${fragUrl}`);
40
- const shaderModule = await this.loader.loadShaderModule(fragUrl);
39
+ shaderModule = await this.loader.loadShaderModule(fragUrl);
41
40
  console.log("GLSLH: Shader successfully parsed!", shaderModule);
42
41
 
43
42
  fragmentShader = shaderModule.source;
44
- uniforms = { ...shaderModule.uniforms, ...uniforms };
43
+ uniforms = shaderModule.uniforms;
45
44
  } catch (err) {
46
45
  console.error("❌ GLSLH Pipeline Error:", err);
47
- // Fallback to bright red if loading fails
48
46
  fragmentShader = `
49
47
  void main() {
50
48
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
@@ -52,7 +50,19 @@ export class GLSLHDomCanvas {
52
50
  `;
53
51
  }
54
52
 
53
+ this.shaderModule = shaderModule;
55
54
  this.uniforms = uniforms;
55
+
56
+ if (this.uniforms.u_resolution && !this.uniforms.u_resolution.value) {
57
+ this.uniforms.u_resolution.value = new THREE.Vector2(
58
+ this.width,
59
+ this.height,
60
+ );
61
+ }
62
+ if (this.uniforms.u_time && this.uniforms.u_time.value === undefined) {
63
+ this.uniforms.u_time.value = 0.0;
64
+ }
65
+
56
66
  this.material = new THREE.ShaderMaterial({
57
67
  vertexShader,
58
68
  fragmentShader,
@@ -70,8 +80,32 @@ export class GLSLHDomCanvas {
70
80
  animate() {
71
81
  requestAnimationFrame(() => this.animate());
72
82
 
83
+ const elapsedTime = this.clock.getElapsedTime();
84
+ const delta = this.clock.getDelta();
85
+
73
86
  if (this.uniforms.u_time) {
74
- this.uniforms.u_time.value = this.clock.getElapsedTime();
87
+ this.uniforms.u_time.value = elapsedTime;
88
+ }
89
+ if (this.uniforms.u_delta) {
90
+ this.uniforms.u_delta.value = delta;
91
+ }
92
+
93
+ if (this.shaderModule && typeof this.shaderModule.update === "function") {
94
+ try {
95
+ this.shaderModule.update(this.uniforms, {
96
+ time: elapsedTime,
97
+ delta: delta,
98
+ width: this.width,
99
+ height: this.height,
100
+ mouseX: 0.0,
101
+ mouseY: 0.0,
102
+ });
103
+ } catch (evalErr) {
104
+ console.error(
105
+ "GLSLH Evaluation Error in derived update loop:",
106
+ evalErr,
107
+ );
108
+ }
75
109
  }
76
110
 
77
111
  this.renderer.render(this.scene, this.camera);
package/src/expr.js ADDED
@@ -0,0 +1,146 @@
1
+ const FUNCS = new Set([
2
+ "sin",
3
+ "cos",
4
+ "tan",
5
+ "asin",
6
+ "acos",
7
+ "atan",
8
+ "atan2",
9
+ "sqrt",
10
+ "abs",
11
+ "pow",
12
+ "exp",
13
+ "log",
14
+ "min",
15
+ "max",
16
+ "floor",
17
+ "ceil",
18
+ "round",
19
+ "sign",
20
+ "hypot",
21
+ ]);
22
+
23
+ const CONSTS = {
24
+ PI: Math.PI,
25
+ TAU: Math.PI * 2,
26
+ E: Math.E,
27
+ SQRT2: Math.SQRT2,
28
+ };
29
+
30
+ export function compileExpr(src) {
31
+ src = src.trim();
32
+ const toks = [];
33
+ const re =
34
+ /\s*(?:(\d*\.?\d+(?:[eE][+-]?\d+)?)|([A-Za-z_][\w.]*)|(\*\*|[-+*/%(),\[\]]))/y;
35
+
36
+ for (let pos = 0; pos < src.length; ) {
37
+ re.lastIndex = pos;
38
+ const m = re.exec(src);
39
+ if (!m) throw new Error(`Bad expression "${src}" near ${pos}`);
40
+ pos = re.lastIndex;
41
+ toks.push(
42
+ m[1] !== undefined
43
+ ? { n: +m[1] }
44
+ : m[2] !== undefined
45
+ ? { id: m[2] }
46
+ : { op: m[3] },
47
+ );
48
+ }
49
+
50
+ let i = 0;
51
+ const deps = new Set();
52
+ const eat = (op) => (toks[i]?.op === op ? (i++, true) : false);
53
+ const expect = (op) => {
54
+ if (!eat(op)) throw new Error(`Expected '${op}' in "${src}"`);
55
+ };
56
+
57
+ const binary = (next, ops) => () => {
58
+ let l = next();
59
+ for (;;) {
60
+ const op = ops.find(eat);
61
+ if (!op) return l;
62
+ const a = l,
63
+ b = next();
64
+ l =
65
+ op === "+"
66
+ ? (c) => a(c) + b(c)
67
+ : op === "-"
68
+ ? (c) => a(c) - b(c)
69
+ : op === "*"
70
+ ? (c) => a(c) * b(c)
71
+ : op === "/"
72
+ ? (c) => a(c) / b(c)
73
+ : (c) => a(c) % b(c);
74
+ }
75
+ };
76
+
77
+ const list = (end) => {
78
+ const xs = [];
79
+ if (!eat(end)) {
80
+ do xs.push(expr());
81
+ while (eat(","));
82
+ expect(end);
83
+ }
84
+ return xs;
85
+ };
86
+
87
+ function primary() {
88
+ const t = toks[i++];
89
+ if (!t) throw new Error(`Unexpected end of "${src}"`);
90
+ if (t.n !== undefined) return () => t.n;
91
+ if (t.op === "(") {
92
+ const e = expr();
93
+ expect(")");
94
+ return e;
95
+ }
96
+ if (t.op === "[") {
97
+ const xs = list("]");
98
+ return (c) => xs.map((f) => f(c));
99
+ }
100
+
101
+ if (t.id) {
102
+ const isMath = t.id.startsWith("Math.");
103
+ const name = isMath ? t.id.slice(5) : t.id;
104
+
105
+ if (eat("(")) {
106
+ if (!FUNCS.has(name)) throw new Error(`Function not allowed: ${t.id}`);
107
+ const args = list(")");
108
+ return (c) => Math[name](...args.map((f) => f(c)));
109
+ }
110
+
111
+ if (isMath) {
112
+ if (!(name in CONSTS)) throw new Error(`Unknown constant: ${t.id}`);
113
+ return () => CONSTS[name];
114
+ }
115
+
116
+ deps.add(name);
117
+ return (c) => {
118
+ if (!(name in c))
119
+ throw new Error(`Unknown variable "${name}" in "${src}"`);
120
+ return c[name];
121
+ };
122
+ }
123
+ throw new Error(`Unexpected '${t.op}' in "${src}"`);
124
+ }
125
+
126
+ function unary() {
127
+ if (eat("-")) {
128
+ const a = unary();
129
+ return (c) => -a(c);
130
+ }
131
+ eat("+");
132
+ const b = primary();
133
+ if (eat("**")) {
134
+ const e = unary();
135
+ return (c) => b(c) ** e(c);
136
+ }
137
+ return b;
138
+ }
139
+
140
+ const term = binary(unary, ["*", "/", "%"]);
141
+ const expr = binary(term, ["+", "-"]);
142
+
143
+ const run = expr();
144
+ if (i !== toks.length) throw new Error(`Unexpected token in "${src}"`);
145
+ return { run, deps: [...deps] };
146
+ }
package/src/loader.js CHANGED
@@ -1,5 +1,17 @@
1
+ "use strict";
1
2
  import * as THREE from "three";
2
3
  import { GLSLHParser } from "./parser.js";
4
+ import { buildDerived } from "./derived.js";
5
+
6
+ const ZERO_DEFAULTS = {
7
+ f: 0,
8
+ i: 0,
9
+ b: false,
10
+ c: [1, 1, 1],
11
+ v2: [0, 0],
12
+ v3: [0, 0, 0],
13
+ v4: [0, 0, 0, 0],
14
+ };
3
15
 
4
16
  export class GLSLHLoader {
5
17
  constructor(parserInstance) {
@@ -8,13 +20,14 @@ export class GLSLHLoader {
8
20
 
9
21
  async loadShaderModule(filePath) {
10
22
  const rawSource = await this.parser.fileResolver(filePath);
11
- // Pass filePath here so the parser knows the starting directory!
12
23
  const parsed = await this.parser.parse(rawSource, filePath);
13
24
  const threeUniforms = this.buildThreeUniforms(parsed.uniforms);
25
+ const updateDerived = buildDerived(parsed.uniforms);
14
26
 
15
27
  return {
16
28
  source: parsed.source,
17
29
  uniforms: threeUniforms,
30
+ update: updateDerived,
18
31
 
19
32
  createMaterial(vertexShaderSource, extraOptions = {}) {
20
33
  return new THREE.ShaderMaterial({
@@ -31,22 +44,31 @@ export class GLSLHLoader {
31
44
  const threeUniforms = {};
32
45
 
33
46
  for (const [key, def] of Object.entries(rawDefs)) {
34
- let val = def.value;
47
+ const type =
48
+ def.type || (key === "u_resolution" || key === "u_mouse" ? "v2" : "f");
49
+ let val =
50
+ def.value !== undefined ? def.value : (ZERO_DEFAULTS[type] ?? 0);
35
51
 
36
- switch (def.type) {
52
+ switch (type) {
37
53
  case "c":
38
54
  val = Array.isArray(val)
39
55
  ? new THREE.Color(...val)
40
56
  : new THREE.Color(val);
41
57
  break;
42
58
  case "v2":
43
- val = Array.isArray(val) ? new THREE.Vector2(...val) : val;
59
+ val = Array.isArray(val)
60
+ ? new THREE.Vector2(...val)
61
+ : new THREE.Vector2(val, val);
44
62
  break;
45
63
  case "v3":
46
- val = Array.isArray(val) ? new THREE.Vector3(...val) : val;
64
+ val = Array.isArray(val)
65
+ ? new THREE.Vector3(...val)
66
+ : new THREE.Vector3(val, val, val);
47
67
  break;
48
68
  case "v4":
49
- val = Array.isArray(val) ? new THREE.Vector4(...val) : val;
69
+ val = Array.isArray(val)
70
+ ? new THREE.Vector4(...val)
71
+ : new THREE.Vector4(val, val, val, val);
50
72
  break;
51
73
  default:
52
74
  val = val;
package/src/parser.js CHANGED
@@ -1,5 +1,23 @@
1
+ export const BUILTINS = {
2
+ u_time: { type: "f", from: (f) => f.time },
3
+ u_delta: { type: "f", from: (f) => f.delta },
4
+ u_resolution: { type: "v2", from: (f) => [f.width, f.height] },
5
+ u_aspect: { type: "f", from: (f) => f.width / f.height },
6
+ u_mouse: { type: "v2", from: (f) => [f.mouseX, f.mouseY] },
7
+ };
8
+
9
+ const GLSL_TYPE = {
10
+ f: "float",
11
+ i: "int",
12
+ b: "bool",
13
+ c: "vec3",
14
+ v2: "vec2",
15
+ v3: "vec3",
16
+ v4: "vec4",
17
+ };
18
+
1
19
  export class GLSLHParser {
2
- constructor(fileResolver) {
20
+ constructor(fileResolver, standardLibraryResolver) {
3
21
  this.fileResolver =
4
22
  fileResolver ||
5
23
  (async (path) => {
@@ -10,42 +28,97 @@ export class GLSLHParser {
10
28
  );
11
29
  return await res.text();
12
30
  });
31
+
32
+ this.standardLibraryResolver =
33
+ standardLibraryResolver ||
34
+ (async (libPath) => {
35
+ const res = await fetch(`https://unpkg.com/glslh/lib/${libPath}`);
36
+ if (!res.ok)
37
+ throw new Error(
38
+ `Could not load standard library module: <${libPath}>`,
39
+ );
40
+ return await res.text();
41
+ });
42
+
13
43
  this.includeCache = new Map();
44
+
45
+ this.allowedExtensions = [
46
+ ".glsl",
47
+ ".glslh",
48
+ ".frag",
49
+ ".vert",
50
+ ".frag.glsl",
51
+ ".vert.glsl",
52
+ ];
14
53
  }
15
54
 
16
- // Robust path resolution that correctly handles subdirectory nesting
17
- resolvePath(parentPath, relativePath) {
55
+ validateExtension(filePath) {
56
+ if (filePath.startsWith("http://") || filePath.startsWith("https://"))
57
+ return;
58
+
59
+ const lowerPath = filePath.toLowerCase();
60
+ const isValid = this.allowedExtensions.some((ext) =>
61
+ lowerPath.endsWith(ext),
62
+ );
63
+ if (!isValid) {
64
+ throw new Error(
65
+ `GLSLH Security Error: Unsupported file extension for include -> '${filePath}'. Allowed extensions: ${this.allowedExtensions.join(", ")}`,
66
+ );
67
+ }
68
+ }
69
+
70
+ resolvePath(parentPath, relativePath, isSystemInclude) {
71
+ if (isSystemInclude) {
72
+ return relativePath;
73
+ }
74
+
18
75
  if (!parentPath) return relativePath;
19
76
 
20
- // Extract directory from parent path (e.g., "./src/examples/shaders/header.glslh" -> "./src/examples/shaders")
77
+ if (relativePath.startsWith("/")) {
78
+ return relativePath.substring(1);
79
+ }
80
+
21
81
  const lastSlash = parentPath.lastIndexOf("/");
22
82
  if (lastSlash === -1) return relativePath;
23
83
  const parentDir = parentPath.substring(0, lastSlash);
84
+ const stack = parentDir ? parentDir.split("/") : [];
85
+ const parts = relativePath.split("/");
24
86
 
25
- // If relative path starts with ./, strip it to join cleanly
26
- const cleanRelative = relativePath.startsWith("./")
27
- ? relativePath.substring(2)
28
- : relativePath;
87
+ for (const part of parts) {
88
+ if (part === "" || part === ".") continue;
89
+ if (part === "..") {
90
+ if (stack.length > 0) stack.pop();
91
+ } else {
92
+ stack.push(part);
93
+ }
94
+ }
29
95
 
30
- return `${parentDir}/${cleanRelative}`;
96
+ return stack.join("/");
31
97
  }
32
98
 
33
99
  async parse(sourceCode, currentFilePath = "") {
34
- return await this._parseRecursive(sourceCode, currentFilePath, new Set());
100
+ const result = await this._parseRecursive(
101
+ sourceCode,
102
+ currentFilePath,
103
+ new Set(),
104
+ );
105
+
106
+ // Run duplicate function collision check on the final flattened source
107
+ detectFunctionCollisions(result.source);
108
+
109
+ return result;
35
110
  }
36
111
 
37
112
  async _parseRecursive(sourceCode, currentFilePath, includedFiles) {
38
113
  let processed = sourceCode;
39
-
40
- // 1. Extract and parse the @uniforms metadata block
41
- const uniformRegex = /\/\*\s*@uniforms([\s\S]*?)\*\//;
42
- const match = processed.match(uniformRegex);
114
+ const uniformRegex = /\/\*\s*@uniforms([\s\S]*?)\*\//g;
115
+ let match;
43
116
  let uniformsDefinition = {};
44
117
 
45
- if (match) {
118
+ while ((match = uniformRegex.exec(sourceCode)) !== null) {
46
119
  try {
47
- uniformsDefinition = JSON.parse(match[1].trim());
48
- processed = processed.replace(uniformRegex, "");
120
+ const parsedBlock = JSON.parse(match[1].trim());
121
+ uniformsDefinition = { ...uniformsDefinition, ...parsedBlock };
49
122
  } catch (e) {
50
123
  console.error(
51
124
  "GLSLH Parser Error: Invalid JSON format inside @uniforms block.",
@@ -53,18 +126,41 @@ export class GLSLHParser {
53
126
  );
54
127
  }
55
128
  }
129
+ processed = processed.replace(uniformRegex, "");
56
130
 
57
- // 2. Resolve C-style #include directives recursively
58
- const includeRegex = /^#include\s+["<](.+)[">]/gm;
131
+ const includeRegex = /^#include\s+(["<])(.+)([">])/gm;
59
132
  let includeMatch;
60
133
  let includes = [];
61
134
 
62
135
  while ((includeMatch = includeRegex.exec(processed)) !== null) {
63
- includes.push({ fullMatch: includeMatch[0], path: includeMatch[1] });
136
+ const openDelimiter = includeMatch[1];
137
+ const incPath = includeMatch[2];
138
+ const closeDelimiter = includeMatch[3];
139
+
140
+ if (
141
+ (openDelimiter === '"' && closeDelimiter !== '"') ||
142
+ (openDelimiter === "<" && closeDelimiter !== ">")
143
+ ) {
144
+ throw new Error(
145
+ `GLSLH Syntax Error: Mismatched include delimiters for '${incPath}'`,
146
+ );
147
+ }
148
+
149
+ includes.push({
150
+ fullMatch: includeMatch[0],
151
+ path: incPath,
152
+ isSystem: openDelimiter === "<",
153
+ });
64
154
  }
65
155
 
66
156
  for (const inc of includes) {
67
- const resolvedPath = this.resolvePath(currentFilePath, inc.path);
157
+ const resolvedPath = this.resolvePath(
158
+ currentFilePath,
159
+ inc.path,
160
+ inc.isSystem,
161
+ );
162
+
163
+ this.validateExtension(resolvedPath);
68
164
 
69
165
  if (includedFiles.has(resolvedPath)) {
70
166
  processed = processed.replace(
@@ -75,31 +171,46 @@ export class GLSLHParser {
75
171
  }
76
172
  includedFiles.add(resolvedPath);
77
173
 
78
- let subSource = this.includeCache.get(resolvedPath);
174
+ let subSource = this.includeCache.get(resolvedPath)?.source;
175
+ let subUniforms = {};
176
+
79
177
  if (!subSource) {
80
- let rawSubText = await this.fileResolver(resolvedPath);
178
+ const rawSubText = inc.isSystem
179
+ ? await this.standardLibraryResolver(resolvedPath)
180
+ : await this.fileResolver(resolvedPath);
181
+
81
182
  const parsedSub = await this._parseRecursive(
82
183
  rawSubText,
83
184
  resolvedPath,
84
185
  includedFiles,
85
186
  );
86
187
  subSource = parsedSub.source;
87
- this.includeCache.set(resolvedPath, subSource);
188
+ subUniforms = parsedSub.uniforms;
189
+ this.includeCache.set(resolvedPath, {
190
+ source: subSource,
191
+ uniforms: subUniforms,
192
+ });
193
+ } else {
194
+ subUniforms = this.includeCache.get(resolvedPath).uniforms;
88
195
  }
89
196
 
197
+ uniformsDefinition = { ...subUniforms, ...uniformsDefinition };
198
+
90
199
  processed = processed.replace(
91
200
  inc.fullMatch,
92
201
  `\n// --- BEGIN INCLUDE: ${resolvedPath} ---\n${subSource}\n// --- END INCLUDE: ${resolvedPath} ---\n`,
93
202
  );
94
203
  }
95
204
 
96
- // 3. Dynamically inject JS Math constants directly into GLSL syntax
205
+ const usedConstants = new Set();
206
+
97
207
  processed = processed.replace(
98
208
  /Math\.([A-Z0-9_]+)/g,
99
209
  (match, constantName) => {
100
- if (constantName === "TAU") return (Math.PI * 2).toString();
101
- if (typeof Math[constantName] === "number")
102
- return Math[constantName].toString();
210
+ if (constantName === "TAU" || typeof Math[constantName] === "number") {
211
+ usedConstants.add(constantName);
212
+ return `MATH_${constantName}`;
213
+ }
103
214
  console.warn(
104
215
  `GLSLH Warning: Unknown Math constant 'Math.${constantName}'`,
105
216
  );
@@ -107,9 +218,152 @@ export class GLSLHParser {
107
218
  },
108
219
  );
109
220
 
221
+ const cleanCodeForScanning = processed.replace(
222
+ /\/\/.*|\/\*[\s\S]*?\*\//g,
223
+ "",
224
+ );
225
+ const usedUniforms = [
226
+ ...new Set(cleanCodeForScanning.match(/\bu_\w+\b/g) ?? []),
227
+ ];
228
+
229
+ for (const name of usedUniforms) {
230
+ if (BUILTINS[name]) {
231
+ uniformsDefinition[name] ??= {
232
+ type: BUILTINS[name].type,
233
+ builtin: true,
234
+ };
235
+ } else if (!uniformsDefinition[name]) {
236
+ console.warn(
237
+ `GLSLH Warning: Uniform '${name}' is used in shader source but not defined in @uniforms or built-ins.`,
238
+ );
239
+ }
240
+ }
241
+
242
+ // 1. Extract #version if present so it always stays at the absolute top
243
+ const versionMatch = processed.match(/^\s*#version[^\n]*\r?\n/);
244
+ let versionHeader = "";
245
+ if (versionMatch) {
246
+ versionHeader = versionMatch[0];
247
+ processed = processed.slice(versionHeader.length);
248
+ }
249
+
250
+ // 2. Build auto-injected math constants defines
251
+ let constantsBlock = "";
252
+ if (usedConstants.size > 0) {
253
+ constantsBlock = "// --- GLSLH Auto-injected Math Constants ---\n";
254
+ for (const constantName of usedConstants) {
255
+ const val = constantName === "TAU" ? Math.PI * 2 : Math[constantName];
256
+ constantsBlock += `#define MATH_${constantName} ${val}\n`;
257
+ }
258
+ constantsBlock += "\n";
259
+ }
260
+
261
+ // 3. Build auto-injected uniform declarations
262
+ let declBlock = "";
263
+ const declLines = Object.entries(uniformsDefinition)
264
+ .filter(
265
+ ([n]) => !new RegExp(`\\buniform\\s+\\w+\\s+${n}\\b`).test(processed),
266
+ )
267
+ .map(([n, d]) => `uniform ${GLSL_TYPE[d.type] ?? "float"} ${n};`);
268
+
269
+ if (declLines.length > 0) {
270
+ declBlock =
271
+ "\n// --- GLSLH Auto-injected Uniform Declarations ---\n" +
272
+ declLines.join("\n") +
273
+ "\n\n";
274
+ }
275
+
276
+ // 4. Re-assemble with #version strictly at the top
277
+ processed = versionHeader + constantsBlock + declBlock + processed;
278
+
110
279
  return {
111
280
  source: processed,
112
281
  uniforms: uniformsDefinition,
113
282
  };
114
283
  }
115
284
  }
285
+
286
+ function detectFunctionCollisions(flattenedSource) {
287
+ const funcRegex =
288
+ /\b(float|int|bool|vec2|vec3|vec4|mat2|mat3|mat4|void)\s+(\w+)\s*\(([^)]*)\)/g;
289
+ const registry = new Map();
290
+ const lines = flattenedSource.split(/\r?\n/);
291
+ let currentFile = "root";
292
+
293
+ for (const line of lines) {
294
+ const beginMatch = line.match(/\/\/\s*---\s*BEGIN INCLUDE:\s*(.+)\s*---/);
295
+ if (beginMatch) {
296
+ currentFile = beginMatch[1].trim();
297
+ continue;
298
+ }
299
+
300
+ let match;
301
+ while ((match = funcRegex.exec(line)) !== null) {
302
+ const returnType = match[1];
303
+ const funcName = match[2];
304
+ const params = match[3].trim().replace(/\s+/g, " ");
305
+
306
+ if (funcName === "main") continue;
307
+ const signatureKey = `${funcName}(${params})`;
308
+
309
+ if (registry.has(signatureKey)) {
310
+ const firstFile = registry.get(signatureKey);
311
+ if (firstFile !== currentFile) {
312
+ console.warn(
313
+ `⚠️ GLSLH Duplicate Definition Warning: Exact signature '${signatureKey}' is defined in both '${firstFile}' and '${currentFile}'. This will cause a compilation clash in WebGL.`,
314
+ );
315
+ }
316
+ } else {
317
+ registry.set(signatureKey, currentFile);
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ export function translateShaderError(
324
+ errorMessage,
325
+ flattenedSource,
326
+ rootFilePath = "shader.glslh",
327
+ ) {
328
+ const lines = flattenedSource.split(/\r?\n/);
329
+ const lineMap = [];
330
+ const fileStack = [rootFilePath];
331
+ const lineCounters = { [rootFilePath]: 0 };
332
+
333
+ for (const line of lines) {
334
+ const beginMatch = line.match(/\/\/\s*---\s*BEGIN INCLUDE:\s*(.+)\s*---/);
335
+ const endMatch = line.match(/\/\/\s*---\s*END INCLUDE:\s*(.+)\s*---/);
336
+
337
+ if (beginMatch) {
338
+ const incPath = beginMatch[1].trim();
339
+ fileStack.push(incPath);
340
+ lineCounters[incPath] = 0;
341
+ lineMap.push({ file: incPath, line: -1, isMarker: true });
342
+ continue;
343
+ } else if (endMatch) {
344
+ fileStack.pop();
345
+ lineMap.push({
346
+ file: fileStack.at(-1),
347
+ line: -1,
348
+ isMarker: true,
349
+ });
350
+ continue;
351
+ }
352
+
353
+ const currentFile = fileStack.at(-1);
354
+ lineCounters[currentFile] = (lineCounters[currentFile] || 0) + 1;
355
+ lineMap.push({ file: currentFile, line: lineCounters[currentFile] });
356
+ }
357
+
358
+ const errorRegex = /(?:ERROR:\s*0:|Line\s+|:\s*0:)(\d+)/gi;
359
+
360
+ return errorMessage.replace(errorRegex, (match, lineNumStr) => {
361
+ const flatLine = Number.parseInt(lineNumStr, 10);
362
+ const mapped = lineMap.at(flatLine - 1);
363
+
364
+ if (mapped && !mapped.isMarker) {
365
+ return `[${mapped.file}:${mapped.line}]`;
366
+ }
367
+ return match;
368
+ });
369
+ }