@triforge/compositor-core 0.1.0 → 0.1.2

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.
@@ -67,7 +67,6 @@ export declare class CompositorOutput {
67
67
  setSize(width: number, height: number): void;
68
68
  dispose(): void;
69
69
  private _compileThree;
70
- private _loadThreePassModules;
71
70
  private _compilePmndrs;
72
71
  }
73
72
  //# sourceMappingURL=CompositorOutput.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CompositorOutput.d.ts","sourceRoot":"","sources":["../../src/core/CompositorOutput.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAAE,KAAK,EAAE,MAAM,EAE7B,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,EAAE,QAAQ,EAAgB,MAAM,eAAe,CAAA;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAW,wBAAwB,CAAA;AAEpE,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,OAAO,CAAC,EAAG,iBAAiB,CAAA;IAC5B,QAAQ,EAAG,aAAa,CAAA;IACxB,KAAK,EAAM,KAAK,CAAA;IAChB,MAAM,EAAK,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,gBAAgB;IAC3B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IAEnC,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,uBAAuB;IAOzC,yDAAyD;IACzD,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAMzB,4DAA4D;IAC5D,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAM5B,wDAAwD;IACxD,IAAI,MAAM,IAAI,SAAS,QAAQ,EAAE,CAAwB;IAEzD,sDAAsD;IACtD,IAAI,UAAU,IAAI,OAAO,CAA0B;IAEnD;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAc9B;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAQd,+DAA+D;IAC/D,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAK5C,OAAO,IAAI,IAAI;YAQD,aAAa;YAgEb,qBAAqB;YA4BrB,cAAc;CAgD7B"}
1
+ {"version":3,"file":"CompositorOutput.d.ts","sourceRoot":"","sources":["../../src/core/CompositorOutput.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAAE,KAAK,EAAE,MAAM,EAE7B,MAAM,OAAO,CAAA;AAUd,OAAO,KAAK,EAAE,QAAQ,EAAgB,MAAM,eAAe,CAAA;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAW,wBAAwB,CAAA;AAEpE,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,OAAO,CAAC,EAAG,iBAAiB,CAAA;IAC5B,QAAQ,EAAG,aAAa,CAAA;IACxB,KAAK,EAAM,KAAK,CAAA;IAChB,MAAM,EAAK,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,gBAAgB;IAC3B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IAEnC,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,uBAAuB;IAOzC,yDAAyD;IACzD,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAMzB,4DAA4D;IAC5D,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAM5B,wDAAwD;IACxD,IAAI,MAAM,IAAI,SAAS,QAAQ,EAAE,CAAwB;IAEzD,sDAAsD;IACtD,IAAI,UAAU,IAAI,OAAO,CAA0B;IAEnD;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAc9B;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAQd,+DAA+D;IAC/D,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAK5C,OAAO,IAAI,IAAI;IAQf,OAAO,CAAC,aAAa;YA+CP,cAAc;CAiD7B"}
@@ -1,4 +1,13 @@
1
1
  import { Vector2, WebGLRenderTarget, LinearFilter, RGBAFormat, HalfFloatType, } from 'three';
2
+ import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
3
+ import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
4
+ import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
5
+ import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
6
+ import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
7
+ import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass.js';
8
+ import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
9
+ import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js';
10
+ import { SSRPass } from 'three/examples/jsm/postprocessing/SSRPass.js';
2
11
  /**
3
12
  * CompositorOutput — terminal node of the compositor graph.
4
13
  *
@@ -60,7 +69,7 @@ export class CompositorOutput {
60
69
  this._composer = await this._compilePmndrs();
61
70
  }
62
71
  else {
63
- this._composer = await this._compileThree();
72
+ this._composer = this._compileThree();
64
73
  }
65
74
  this._compiled = true;
66
75
  }
@@ -88,31 +97,18 @@ export class CompositorOutput {
88
97
  this._compiled = false;
89
98
  }
90
99
  // ── Three.js backend ──────────────────────────────────────────────────────
91
- async _compileThree() {
92
- // Dynamically import three/addons so this package has no hard dependency
93
- // on a specific addons path — works with CDN importmap and npm alike.
94
- let EC;
95
- let RenderPass;
96
- let OutputPass;
97
- try {
98
- const [ecMod, rpMod, opMod] = await Promise.all([
99
- import('three/addons/postprocessing/EffectComposer.js'),
100
- import('three/addons/postprocessing/RenderPass.js'),
101
- import('three/addons/postprocessing/OutputPass.js'),
102
- ]);
103
- EC = ecMod['EffectComposer'];
104
- RenderPass = rpMod['RenderPass'];
105
- OutputPass = opMod['OutputPass'];
106
- }
107
- catch {
108
- throw new Error('@st-compositor-core: Could not load three/addons postprocessing modules.\n' +
109
- 'Make sure "three/addons/" is in your importmap or install three with examples/jsm.\n' +
110
- 'Example importmap entry:\n' +
111
- ' "three/addons/": "https://unpkg.com/three@0.165.0/examples/jsm/"');
112
- }
100
+ _compileThree() {
113
101
  const activePasses = this._passes.filter(p => p.enabled);
114
- // Load all required per-pass three/addons modules into one registry object
115
- const registry = await this._loadThreePassModules(activePasses);
102
+ // Registry built from static imports — compatible with Vite, Webpack, Rollup, esbuild
103
+ const registry = {
104
+ ShaderPass,
105
+ UnrealBloomPass,
106
+ FilmPass,
107
+ BokehPass,
108
+ SSAOPass,
109
+ SSRPass,
110
+ RenderPass,
111
+ };
116
112
  const size = new Vector2();
117
113
  this._renderer.getSize(size);
118
114
  const target = new WebGLRenderTarget(size.x, size.y, {
@@ -121,47 +117,27 @@ export class CompositorOutput {
121
117
  format: RGBAFormat,
122
118
  type: HalfFloatType,
123
119
  });
124
- const composer = new EC(this._renderer, target);
120
+ const composer = new EffectComposer(this._renderer, target);
125
121
  const addPass = (p) => composer.addPass(p);
126
- // RenderPass first — renders the scene into the first buffer
127
122
  addPass(new RenderPass(this._scene, this._camera));
128
123
  registry['_renderer'] = this._renderer;
129
124
  registry['_scene'] = this._scene;
130
125
  registry['_camera'] = this._camera;
131
- // User passes
132
126
  for (const pass of activePasses) {
133
127
  const built = pass._buildThree(size.x, size.y, registry);
134
128
  if (built !== null)
135
129
  addPass(built);
136
130
  }
137
- // OutputPass last — required since Three.js r152 to write to screen correctly
131
+ // Required since Three.js r152 to write to screen correctly
138
132
  addPass(new OutputPass());
139
133
  return composer;
140
134
  }
141
- async _loadThreePassModules(passes) {
142
- const registry = {};
143
- const needed = new Set();
144
- for (const pass of passes) {
145
- for (const dep of pass._threePassDeps()) {
146
- needed.add(dep);
147
- }
148
- }
149
- await Promise.all([...needed].map(async (dep) => {
150
- try {
151
- const mod = await import(`three/addons/postprocessing/${dep}.js`);
152
- Object.assign(registry, mod);
153
- }
154
- catch {
155
- // Pass will throw its own clear error in _buildThree() if dep is missing
156
- }
157
- }));
158
- return registry;
159
- }
160
135
  // ── pmndrs backend ────────────────────────────────────────────────────────
161
136
  async _compilePmndrs() {
162
137
  let pkg;
163
138
  try {
164
- pkg = await import('postprocessing');
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
+ pkg = await Function('m', 'return import(m)')('postprocessing');
165
141
  }
166
142
  catch {
167
143
  throw new Error('@st-compositor-core: backend "pmndrs" requires the "postprocessing" package.\n' +
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Compile-time GLSL loop-bound sanitizer.
3
+ *
4
+ * Several passes bake a JS number directly into a GLSL `for` loop bound, e.g.
5
+ * `for (int i = 0; i < ${n}; i++)`. Per the root ecosystem rule ("ALWAYS cap
6
+ * GLSL loop iterations to a compile-time constant"), that number must be a
7
+ * finite integer within a sane range before it reaches the shader source:
8
+ *
9
+ * - `NaN`/`Infinity` would produce a shader that fails to compile.
10
+ * - An absurdly large value (e.g. a mistyped `samples: 1e6`) would compile a
11
+ * loop that hangs the GPU.
12
+ *
13
+ * This clamps to [min, max] and rounds to an integer, so the emitted bound is
14
+ * always a small compile-time constant.
15
+ */
16
+ export declare function glslLoopBound(value: number, min: number, max: number): number;
17
+ //# sourceMappingURL=glslLoopBound.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glslLoopBound.d.ts","sourceRoot":"","sources":["../../src/core/glslLoopBound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM7E"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Compile-time GLSL loop-bound sanitizer.
3
+ *
4
+ * Several passes bake a JS number directly into a GLSL `for` loop bound, e.g.
5
+ * `for (int i = 0; i < ${n}; i++)`. Per the root ecosystem rule ("ALWAYS cap
6
+ * GLSL loop iterations to a compile-time constant"), that number must be a
7
+ * finite integer within a sane range before it reaches the shader source:
8
+ *
9
+ * - `NaN`/`Infinity` would produce a shader that fails to compile.
10
+ * - An absurdly large value (e.g. a mistyped `samples: 1e6`) would compile a
11
+ * loop that hangs the GPU.
12
+ *
13
+ * This clamps to [min, max] and rounds to an integer, so the emitted bound is
14
+ * always a small compile-time constant.
15
+ */
16
+ export function glslLoopBound(value, min, max) {
17
+ if (!Number.isFinite(value))
18
+ return min;
19
+ const rounded = Math.round(value);
20
+ if (rounded < min)
21
+ return min;
22
+ if (rounded > max)
23
+ return max;
24
+ return rounded;
25
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"GlareStreaks.d.ts","sourceRoot":"","sources":["../../src/passes/GlareStreaks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,MAAM,WAAW,mBAAmB;IAClC,qGAAqG;IACrG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;GAMG;AACH,qBAAa,YAAa,SAAQ,QAAQ;IACxC,QAAQ,CAAC,QAAQ,kBAAiB;IAClC,UAAU,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;gBAErG,IAAI,GAAE,mBAAwB;IAY1C,cAAc;IAEd,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;CA8DhE"}
1
+ {"version":3,"file":"GlareStreaks.d.ts","sourceRoot":"","sources":["../../src/passes/GlareStreaks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAGvD,MAAM,WAAW,mBAAmB;IAClC,qGAAqG;IACrG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;GAMG;AACH,qBAAa,YAAa,SAAQ,QAAQ;IACxC,QAAQ,CAAC,QAAQ,kBAAiB;IAClC,UAAU,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;gBAErG,IAAI,GAAE,mBAAwB;IAY1C,cAAc;IAEd,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;CAmEhE"}
@@ -1,4 +1,5 @@
1
1
  import { BasePass } from '../core/BasePass.js';
2
+ import { glslLoopBound } from '../core/glslLoopBound.js';
2
3
  /**
3
4
  * GlareStreaks — anamorphic lens streak effect on bright areas.
4
5
  * Blender Compositor: Glare node (Streaks type).
@@ -24,7 +25,12 @@ export class GlareStreaks extends BasePass {
24
25
  const ShaderPass = reg['ShaderPass'];
25
26
  if (!ShaderPass)
26
27
  throw new Error('GlareStreaks: ShaderPass not found.');
27
- const { threshold, strength, streaks, length, angle, fade } = this.parameters;
28
+ const { threshold, strength, length, angle, fade } = this.parameters;
29
+ // Cap both baked loop bounds: streak count (1..16) and per-streak sample
30
+ // length (1..128) — both are interpolated into generated GLSL / unrolled
31
+ // JS below, so both must be finite compile-time-bounded integers.
32
+ const streaks = glslLoopBound(this.parameters.streaks, 1, 16);
33
+ const sampleLength = glslLoopBound(length, 1, 128);
28
34
  // Build direction vectors for each streak
29
35
  const dirs = [];
30
36
  for (let i = 0; i < streaks; i++) {
@@ -40,41 +46,41 @@ export class GlareStreaks extends BasePass {
40
46
  fade: { value: fade },
41
47
  length: { value: length },
42
48
  },
43
- vertexShader: `
44
- varying vec2 vUv;
45
- void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }
49
+ vertexShader: `
50
+ varying vec2 vUv;
51
+ void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }
46
52
  `,
47
- fragmentShader: `
48
- uniform sampler2D tDiffuse;
49
- uniform vec2 resolution;
50
- uniform float threshold;
51
- uniform float strength;
52
- uniform float fade;
53
- uniform float length;
54
- varying vec2 vUv;
55
-
56
- float luminance(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
57
-
58
- vec4 sampleStreak(vec2 dir) {
59
- vec4 acc = vec4(0.0);
60
- float att = 1.0;
61
- vec2 step = dir / resolution;
62
- for (int i = 1; i <= ${Math.max(1, Math.round(length))}; i++) {
63
- vec2 uv = vUv + step * float(i) * strength;
64
- vec4 samp = texture2D(tDiffuse, clamp(uv, 0.0, 1.0));
65
- float lum = luminance(samp.rgb);
66
- if (lum > threshold) acc += samp * att;
67
- att *= fade;
68
- }
69
- return acc;
70
- }
71
-
72
- void main() {
73
- vec4 base = texture2D(tDiffuse, vUv);
74
- vec4 glare = vec4(0.0);
75
- ${dirs.map(d => `glare += sampleStreak(${d});`).join('\n ')}
76
- gl_FragColor = base + glare * (1.0 / ${Math.max(1, streaks)}.0);
77
- }
53
+ fragmentShader: `
54
+ uniform sampler2D tDiffuse;
55
+ uniform vec2 resolution;
56
+ uniform float threshold;
57
+ uniform float strength;
58
+ uniform float fade;
59
+ uniform float length;
60
+ varying vec2 vUv;
61
+
62
+ float luminance(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
63
+
64
+ vec4 sampleStreak(vec2 dir) {
65
+ vec4 acc = vec4(0.0);
66
+ float att = 1.0;
67
+ vec2 step = dir / resolution;
68
+ for (int i = 1; i <= ${sampleLength}; i++) {
69
+ vec2 uv = vUv + step * float(i) * strength;
70
+ vec4 samp = texture2D(tDiffuse, clamp(uv, 0.0, 1.0));
71
+ float lum = luminance(samp.rgb);
72
+ if (lum > threshold) acc += samp * att;
73
+ att *= fade;
74
+ }
75
+ return acc;
76
+ }
77
+
78
+ void main() {
79
+ vec4 base = texture2D(tDiffuse, vUv);
80
+ vec4 glare = vec4(0.0);
81
+ ${dirs.map(d => `glare += sampleStreak(${d});`).join('\n ')}
82
+ gl_FragColor = base + glare * (1.0 / ${Math.max(1, streaks)}.0);
83
+ }
78
84
  `,
79
85
  };
80
86
  return new ShaderPass(shader);
@@ -1 +1 @@
1
- {"version":3,"file":"LensFlare.d.ts","sourceRoot":"","sources":["../../src/passes/LensFlare.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,QAAQ;IACrC,QAAQ,CAAC,QAAQ,eAAc;IAC/B,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;gBAE7F,IAAI,GAAE,gBAAqB;IAWvC,cAAc;IAEd,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;CAyEhE"}
1
+ {"version":3,"file":"LensFlare.d.ts","sourceRoot":"","sources":["../../src/passes/LensFlare.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAGvD,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,QAAQ;IACrC,QAAQ,CAAC,QAAQ,eAAc;IAC/B,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;gBAE7F,IAAI,GAAE,gBAAqB;IAWvC,cAAc;IAEd,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;CA0EhE"}
@@ -1,4 +1,5 @@
1
1
  import { BasePass } from '../core/BasePass.js';
2
+ import { glslLoopBound } from '../core/glslLoopBound.js';
2
3
  /**
3
4
  * LensFlare — Blender Compositor: Lens Flare / Glare node (lens reflections type).
4
5
  *
@@ -23,7 +24,8 @@ export class LensFlare extends BasePass {
23
24
  if (!ShaderPass)
24
25
  throw new Error('LensFlare: ShaderPass not found.');
25
26
  const { ghosts, strength, threshold, haloWidth, chromatic } = this.parameters;
26
- const n = Math.max(1, Math.round(ghosts));
27
+ // Cap the baked loop bound: finite, integer, 1..32 ghost reflections.
28
+ const n = glslLoopBound(ghosts, 1, 32);
27
29
  const shader = {
28
30
  uniforms: {
29
31
  tDiffuse: { value: null },
@@ -32,59 +34,59 @@ export class LensFlare extends BasePass {
32
34
  haloWidth: { value: haloWidth },
33
35
  chromatic: { value: chromatic },
34
36
  },
35
- vertexShader: `
36
- varying vec2 vUv;
37
- void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }
37
+ vertexShader: `
38
+ varying vec2 vUv;
39
+ void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }
38
40
  `,
39
- fragmentShader: `
40
- uniform sampler2D tDiffuse;
41
- uniform float strength;
42
- uniform float threshold;
43
- uniform float haloWidth;
44
- uniform float chromatic;
45
- varying vec2 vUv;
46
-
47
- float luminance(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
48
-
49
- vec3 sampleChromatic(vec2 uv, vec2 offset) {
50
- vec2 r = clamp(uv + offset * (1.0 + chromatic * 0.1), 0.0, 1.0);
51
- vec2 b = clamp(uv + offset * (1.0 - chromatic * 0.1), 0.0, 1.0);
52
- return vec3(
53
- texture2D(tDiffuse, r).r,
54
- texture2D(tDiffuse, clamp(uv + offset, 0.0, 1.0)).g,
55
- texture2D(tDiffuse, b).b
56
- );
57
- }
58
-
59
- void main() {
60
- vec4 base = texture2D(tDiffuse, vUv);
61
- vec3 flare = vec3(0.0);
62
- vec2 ghost = vec2(0.5) - vUv; // vector to screen centre
63
-
64
- // Ghost reflections along the screen diagonal
65
- for (int i = 1; i <= ${n}; i++) {
66
- float fi = float(i);
67
- float disp = 0.3 + fi * 0.25;
68
- vec2 guv = vUv + ghost * disp;
69
- vec3 samp = sampleChromatic(guv, ghost * 0.01 * fi);
70
- float lum = luminance(samp);
71
- if (lum > threshold) {
72
- vec3 tint = mix(vec3(1.0, 0.8, 0.6), vec3(0.6, 0.8, 1.0), fract(fi * 0.37));
73
- flare += samp * tint * (lum - threshold) * strength / fi;
74
- }
75
- }
76
-
77
- // Halo ring
78
- float dist = length(ghost);
79
- float haloBias = abs(dist - haloWidth);
80
- float halo = smoothstep(0.05, 0.0, haloBias) * strength * 0.5;
81
- vec4 haloCentre = texture2D(tDiffuse, clamp(vec2(0.5) + normalize(-ghost) * haloWidth, 0.0, 1.0));
82
- if (luminance(haloCentre.rgb) > threshold) {
83
- flare += haloCentre.rgb * halo;
84
- }
85
-
86
- gl_FragColor = vec4(base.rgb + flare, base.a);
87
- }
41
+ fragmentShader: `
42
+ uniform sampler2D tDiffuse;
43
+ uniform float strength;
44
+ uniform float threshold;
45
+ uniform float haloWidth;
46
+ uniform float chromatic;
47
+ varying vec2 vUv;
48
+
49
+ float luminance(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
50
+
51
+ vec3 sampleChromatic(vec2 uv, vec2 offset) {
52
+ vec2 r = clamp(uv + offset * (1.0 + chromatic * 0.1), 0.0, 1.0);
53
+ vec2 b = clamp(uv + offset * (1.0 - chromatic * 0.1), 0.0, 1.0);
54
+ return vec3(
55
+ texture2D(tDiffuse, r).r,
56
+ texture2D(tDiffuse, clamp(uv + offset, 0.0, 1.0)).g,
57
+ texture2D(tDiffuse, b).b
58
+ );
59
+ }
60
+
61
+ void main() {
62
+ vec4 base = texture2D(tDiffuse, vUv);
63
+ vec3 flare = vec3(0.0);
64
+ vec2 ghost = vec2(0.5) - vUv; // vector to screen centre
65
+
66
+ // Ghost reflections along the screen diagonal
67
+ for (int i = 1; i <= ${n}; i++) {
68
+ float fi = float(i);
69
+ float disp = 0.3 + fi * 0.25;
70
+ vec2 guv = vUv + ghost * disp;
71
+ vec3 samp = sampleChromatic(guv, ghost * 0.01 * fi);
72
+ float lum = luminance(samp);
73
+ if (lum > threshold) {
74
+ vec3 tint = mix(vec3(1.0, 0.8, 0.6), vec3(0.6, 0.8, 1.0), fract(fi * 0.37));
75
+ flare += samp * tint * (lum - threshold) * strength / fi;
76
+ }
77
+ }
78
+
79
+ // Halo ring
80
+ float dist = length(ghost);
81
+ float haloBias = abs(dist - haloWidth);
82
+ float halo = smoothstep(0.05, 0.0, haloBias) * strength * 0.5;
83
+ vec4 haloCentre = texture2D(tDiffuse, clamp(vec2(0.5) + normalize(-ghost) * haloWidth, 0.0, 1.0));
84
+ if (luminance(haloCentre.rgb) > threshold) {
85
+ flare += haloCentre.rgb * halo;
86
+ }
87
+
88
+ gl_FragColor = vec4(base.rgb + flare, base.a);
89
+ }
88
90
  `,
89
91
  };
90
92
  return new ShaderPass(shader);
@@ -1 +1 @@
1
- {"version":3,"file":"MotionBlur.d.ts","sourceRoot":"","sources":["../../src/passes/MotionBlur.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;GAQG;AACH,qBAAa,UAAW,SAAQ,QAAQ;IACtC,QAAQ,CAAC,QAAQ,gBAAe;IAChC,UAAU,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;gBAE7D,IAAI,GAAE,iBAAsB;IASxC,cAAc;IAEd,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;CA6ChE"}
1
+ {"version":3,"file":"MotionBlur.d.ts","sourceRoot":"","sources":["../../src/passes/MotionBlur.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAGvD,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;GAQG;AACH,qBAAa,UAAW,SAAQ,QAAQ;IACtC,QAAQ,CAAC,QAAQ,gBAAe;IAChC,UAAU,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;gBAE7D,IAAI,GAAE,iBAAsB;IASxC,cAAc;IAEd,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;CA+ChE"}
@@ -1,4 +1,5 @@
1
1
  import { BasePass } from '../core/BasePass.js';
2
+ import { glslLoopBound } from '../core/glslLoopBound.js';
2
3
  /**
3
4
  * MotionBlur — screen-space radial/directional motion blur.
4
5
  * Blender Compositor: Vector Blur node.
@@ -24,7 +25,9 @@ export class MotionBlur extends BasePass {
24
25
  if (!ShaderPass)
25
26
  throw new Error('MotionBlur: ShaderPass not found.');
26
27
  const { samples, velocityScale, maxRadius } = this.parameters;
27
- const n = Math.max(1, Math.round(samples));
28
+ // Cap the baked loop bound: finite, integer, 1..64 (matches typical
29
+ // real-time motion-blur sample budgets; guards against NaN / huge values).
30
+ const n = glslLoopBound(samples, 1, 64);
28
31
  const shader = {
29
32
  uniforms: {
30
33
  tDiffuse: { value: null },
@@ -32,32 +35,32 @@ export class MotionBlur extends BasePass {
32
35
  velocityScale: { value: velocityScale },
33
36
  maxRadius: { value: maxRadius },
34
37
  },
35
- vertexShader: `
36
- varying vec2 vUv;
37
- void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }
38
+ vertexShader: `
39
+ varying vec2 vUv;
40
+ void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }
38
41
  `,
39
- fragmentShader: `
40
- uniform sampler2D tDiffuse;
41
- uniform vec2 resolution;
42
- uniform float velocityScale;
43
- uniform float maxRadius;
44
- varying vec2 vUv;
45
-
46
- void main() {
47
- // Screen-space radial blur toward centre
48
- vec2 centre = vec2(0.5);
49
- vec2 vel = (centre - vUv) * velocityScale;
50
- float len = length(vel * resolution);
51
- if (len > maxRadius) vel = vel * (maxRadius / max(len, 0.001));
52
-
53
- vec4 acc = vec4(0.0);
54
- for (int i = 0; i < ${n}; i++) {
55
- float t = float(i) / float(${n - 1});
56
- vec2 uv = clamp(vUv + vel * t / resolution, 0.0, 1.0);
57
- acc += texture2D(tDiffuse, uv);
58
- }
59
- gl_FragColor = acc / float(${n});
60
- }
42
+ fragmentShader: `
43
+ uniform sampler2D tDiffuse;
44
+ uniform vec2 resolution;
45
+ uniform float velocityScale;
46
+ uniform float maxRadius;
47
+ varying vec2 vUv;
48
+
49
+ void main() {
50
+ // Screen-space radial blur toward centre
51
+ vec2 centre = vec2(0.5);
52
+ vec2 vel = (centre - vUv) * velocityScale;
53
+ float len = length(vel * resolution);
54
+ if (len > maxRadius) vel = vel * (maxRadius / max(len, 0.001));
55
+
56
+ vec4 acc = vec4(0.0);
57
+ for (int i = 0; i < ${n}; i++) {
58
+ float t = float(i) / float(${n - 1});
59
+ vec2 uv = clamp(vUv + vel * t / resolution, 0.0, 1.0);
60
+ acc += texture2D(tDiffuse, uv);
61
+ }
62
+ gl_FragColor = acc / float(${n});
63
+ }
61
64
  `,
62
65
  };
63
66
  return new ShaderPass(shader);
package/package.json CHANGED
@@ -1,48 +1,54 @@
1
- {
2
- "name": "@triforge/compositor-core",
3
- "version": "0.1.0",
4
- "description": "Blender-matched compositor node graph for Three.js — backend-agnostic (three/addons or pmndrs/postprocessing)",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "files": [
9
- "dist",
10
- "package.json"
11
- ],
12
- "scripts": {
13
- "build": "tsc",
14
- "dev": "tsc --watch",
15
- "test": "node test/run-tests.js"
16
- },
17
- "keywords": [
18
- "three.js",
19
- "post-processing",
20
- "compositor",
21
- "blender",
22
- "effects"
23
- ],
24
- "author": "Aftab Ibrahim Kazi",
25
- "license": "MIT",
26
- "peerDependencies": {
27
- "three": ">=0.128.0"
28
- },
29
- "peerDependenciesMeta": {
30
- "postprocessing": {
31
- "optional": true
32
- }
33
- },
34
- "devDependencies": {
35
- "@types/three": "^0.156.0",
36
- "three": "^0.156.0",
37
- "typescript": "^5.3.0"
38
- },
39
- "repository": {
40
- "type": "git",
41
- "url": "https://github.com/AftabIbrahimKazi/triforge.git",
42
- "directory": "st-compositor-core"
43
- },
44
- "homepage": "https://github.com/AftabIbrahimKazi/triforge#readme",
45
- "publishConfig": {
46
- "access": "public"
47
- }
48
- }
1
+ {
2
+ "name": "@triforge/compositor-core",
3
+ "version": "0.1.2",
4
+ "description": "Blender-matched compositor node graph for Three.js — backend-agnostic (three/addons or pmndrs/postprocessing)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "package.json"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch",
15
+ "test": "node test/run-tests.js"
16
+ },
17
+ "keywords": [
18
+ "three.js",
19
+ "post-processing",
20
+ "compositor",
21
+ "blender",
22
+ "effects"
23
+ ],
24
+ "author": "Aftab Ibrahim Kazi",
25
+ "license": "MIT",
26
+ "peerDependencies": {
27
+ "three": ">=0.128.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "postprocessing": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "devDependencies": {
35
+ "@types/three": "^0.156.0",
36
+ "three": "^0.156.0",
37
+ "typescript": "^5.3.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/AftabIbrahimKazi/triforge.git",
42
+ "directory": "st-compositor-core"
43
+ },
44
+ "homepage": "https://github.com/AftabIbrahimKazi/triforge#readme",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "exports": {
49
+ ".": {
50
+ "import": "./dist/index.js",
51
+ "types": "./dist/index.d.ts"
52
+ }
53
+ }
54
+ }