glslh 1.0.1 → 1.1.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/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  ## 📋 `glslh` Changelog
2
2
 
3
- ### **v1.0.1** (Latest)
3
+ ### **v1.1.1** (Latest)
4
+
5
+ - **Documentation & README Refinement**: Updated documentation, architecture diagrams, and release logs to reflect complete structural guides for modular struct-based composition, CPU/GPU division of labor, and extension workflows.
6
+
7
+ ---
8
+
9
+ ### **v1.1.0**
10
+
11
+ - **Duplicate Function Collision Detection & Overloading Support**: Added automated static analysis to scan included modules and warn developers if conflicting function signatures clash, while fully supporting valid GLSL function overloading distinguished by parameter signatures.
12
+ - **Strict `#version` Directive Pinning**: Upgraded the preprocessor parsing pipeline to extract and pin `#version` directives to the absolute top of the generated string, ensuring full compliance with strict WebGL driver standards regardless of auto-injected uniform declarations or math constants.
13
+ - **Enhanced Error Line Mapping**: Improved error translation utilities to map monolithic WebGL compilation error line numbers back to their exact source file and line number (`[shape_utils.glsl:14]`).
14
+
15
+ ---
16
+
17
+ ### **v1.0.1**
4
18
 
5
19
  - **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
20
  - **Compound Header Support**: Enhanced path checking to natively support complex shader extension combinations like `.frag.glslh` and `.vert.glslh`.
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, while fully supporting valid GLSL function overloading.
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,48 +65,53 @@ 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
 
75
93
  ## 🚀 Installation & Quick Start
76
94
 
77
95
  ```bash
78
- npm install glslh
96
+ npm install glslh@1.1.1
79
97
  ```
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)
84
-
85
101
  ### 2. Run Declaratively in HTML (`index.html`)
86
102
 
87
- Refer to the complete working example setup here: [index.html](./test/index.html)
88
-
89
103
  ---
90
104
 
91
- ## 📂 Project Structure
105
+ ## ⚡ VS Code Extension (Live Preview)
92
106
 
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
107
+ Want instant syntax highlighting, color pickers, and a live WebGL preview pane right inside your editor? Install the official [**GLSLH Previewer**](https://marketplace.visualstudio.com/items?itemName=SudhanshuAmbastha.glslh-previewer) extension from the marketplace:
101
108
 
102
- ```
109
+ - **Live WebGL Preview:** Inspect shaders side-by-side with automatic hot-reloading on save.
110
+ - **Native Color Pickers:** Click color definitions or uniform arrays in `@uniforms` or `vec3()` blocks to adjust values visually.
111
+ - **Multi-file Resolution:** Seamlessly traces `#include` dependencies across your project directory.
103
112
 
104
113
  ---
105
114
 
106
115
  ## 📜 License
107
116
 
108
- Distributed under the **MIT License**. See `LICENSE` for more information.
117
+ Distributed under the **MIT License**. See [`LICENSE`](../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.1",
3
+ "version": "1.1.1",
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,6 +28,18 @@ 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();
14
44
 
15
45
  this.allowedExtensions = [
@@ -23,6 +53,9 @@ export class GLSLHParser {
23
53
  }
24
54
 
25
55
  validateExtension(filePath) {
56
+ if (filePath.startsWith("http://") || filePath.startsWith("https://"))
57
+ return;
58
+
26
59
  const lowerPath = filePath.toLowerCase();
27
60
  const isValid = this.allowedExtensions.some((ext) =>
28
61
  lowerPath.endsWith(ext),
@@ -34,34 +67,58 @@ export class GLSLHParser {
34
67
  }
35
68
  }
36
69
 
37
- resolvePath(parentPath, relativePath) {
70
+ resolvePath(parentPath, relativePath, isSystemInclude) {
71
+ if (isSystemInclude) {
72
+ return relativePath;
73
+ }
74
+
38
75
  if (!parentPath) return relativePath;
39
76
 
77
+ if (relativePath.startsWith("/")) {
78
+ return relativePath.substring(1);
79
+ }
80
+
40
81
  const lastSlash = parentPath.lastIndexOf("/");
41
82
  if (lastSlash === -1) return relativePath;
42
83
  const parentDir = parentPath.substring(0, lastSlash);
84
+ const stack = parentDir ? parentDir.split("/") : [];
85
+ const parts = relativePath.split("/");
43
86
 
44
- const cleanRelative = relativePath.startsWith("./")
45
- ? relativePath.substring(2)
46
- : 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
+ }
47
95
 
48
- return `${parentDir}/${cleanRelative}`;
96
+ return stack.join("/");
49
97
  }
50
98
 
51
99
  async parse(sourceCode, currentFilePath = "") {
52
- 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;
53
110
  }
54
111
 
55
112
  async _parseRecursive(sourceCode, currentFilePath, includedFiles) {
56
113
  let processed = sourceCode;
57
- const uniformRegex = /\/\*\s*@uniforms([\s\S]*?)\*\//;
58
- const match = processed.match(uniformRegex);
114
+ const uniformRegex = /\/\*\s*@uniforms([\s\S]*?)\*\//g;
115
+ let match;
59
116
  let uniformsDefinition = {};
60
117
 
61
- if (match) {
118
+ while ((match = uniformRegex.exec(sourceCode)) !== null) {
62
119
  try {
63
- uniformsDefinition = JSON.parse(match[1].trim());
64
- processed = processed.replace(uniformRegex, "");
120
+ const parsedBlock = JSON.parse(match[1].trim());
121
+ uniformsDefinition = { ...uniformsDefinition, ...parsedBlock };
65
122
  } catch (e) {
66
123
  console.error(
67
124
  "GLSLH Parser Error: Invalid JSON format inside @uniforms block.",
@@ -69,17 +126,39 @@ export class GLSLHParser {
69
126
  );
70
127
  }
71
128
  }
129
+ processed = processed.replace(uniformRegex, "");
72
130
 
73
- const includeRegex = /^#include\s+["<](.+)[">]/gm;
131
+ const includeRegex = /^#include\s+(["<])(.+)([">])/gm;
74
132
  let includeMatch;
75
133
  let includes = [];
76
134
 
77
135
  while ((includeMatch = includeRegex.exec(processed)) !== null) {
78
- 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
+ });
79
154
  }
80
155
 
81
156
  for (const inc of includes) {
82
- const resolvedPath = this.resolvePath(currentFilePath, inc.path);
157
+ const resolvedPath = this.resolvePath(
158
+ currentFilePath,
159
+ inc.path,
160
+ inc.isSystem,
161
+ );
83
162
 
84
163
  this.validateExtension(resolvedPath);
85
164
 
@@ -92,30 +171,46 @@ export class GLSLHParser {
92
171
  }
93
172
  includedFiles.add(resolvedPath);
94
173
 
95
- let subSource = this.includeCache.get(resolvedPath);
174
+ let subSource = this.includeCache.get(resolvedPath)?.source;
175
+ let subUniforms = {};
176
+
96
177
  if (!subSource) {
97
- let rawSubText = await this.fileResolver(resolvedPath);
178
+ const rawSubText = inc.isSystem
179
+ ? await this.standardLibraryResolver(resolvedPath)
180
+ : await this.fileResolver(resolvedPath);
181
+
98
182
  const parsedSub = await this._parseRecursive(
99
183
  rawSubText,
100
184
  resolvedPath,
101
185
  includedFiles,
102
186
  );
103
187
  subSource = parsedSub.source;
104
- 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;
105
195
  }
106
196
 
197
+ uniformsDefinition = { ...subUniforms, ...uniformsDefinition };
198
+
107
199
  processed = processed.replace(
108
200
  inc.fullMatch,
109
201
  `\n// --- BEGIN INCLUDE: ${resolvedPath} ---\n${subSource}\n// --- END INCLUDE: ${resolvedPath} ---\n`,
110
202
  );
111
203
  }
112
204
 
205
+ const usedConstants = new Set();
206
+
113
207
  processed = processed.replace(
114
208
  /Math\.([A-Z0-9_]+)/g,
115
209
  (match, constantName) => {
116
- if (constantName === "TAU") return (Math.PI * 2).toString();
117
- if (typeof Math[constantName] === "number")
118
- return Math[constantName].toString();
210
+ if (constantName === "TAU" || typeof Math[constantName] === "number") {
211
+ usedConstants.add(constantName);
212
+ return `MATH_${constantName}`;
213
+ }
119
214
  console.warn(
120
215
  `GLSLH Warning: Unknown Math constant 'Math.${constantName}'`,
121
216
  );
@@ -123,9 +218,152 @@ export class GLSLHParser {
123
218
  },
124
219
  );
125
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
+
126
279
  return {
127
280
  source: processed,
128
281
  uniforms: uniformsDefinition,
129
282
  };
130
283
  }
131
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
+ }