@vector-display/beam-fx 0.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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/FlickerGenerator.d.ts +10 -0
- package/dist/FlickerGenerator.d.ts.map +1 -0
- package/dist/FlickerGenerator.js +29 -0
- package/dist/FlickerGenerator.js.map +1 -0
- package/dist/FullscreenPass.d.ts +13 -0
- package/dist/FullscreenPass.d.ts.map +1 -0
- package/dist/FullscreenPass.js +42 -0
- package/dist/FullscreenPass.js.map +1 -0
- package/dist/PhosphorPipeline.d.ts +42 -0
- package/dist/PhosphorPipeline.d.ts.map +1 -0
- package/dist/PhosphorPipeline.js +162 -0
- package/dist/PhosphorPipeline.js.map +1 -0
- package/dist/RenderTarget.d.ts +17 -0
- package/dist/RenderTarget.d.ts.map +1 -0
- package/dist/RenderTarget.js +40 -0
- package/dist/RenderTarget.js.map +1 -0
- package/dist/effectShaders.d.ts +4 -0
- package/dist/effectShaders.d.ts.map +1 -0
- package/dist/effectShaders.js +47 -0
- package/dist/effectShaders.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
- package/src/FlickerGenerator.ts +30 -0
- package/src/FullscreenPass.ts +49 -0
- package/src/PhosphorPipeline.ts +199 -0
- package/src/RenderTarget.ts +50 -0
- package/src/effectShaders.ts +48 -0
- package/src/index.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rick Segrest
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @vector-display/beam-fx
|
|
2
|
+
|
|
3
|
+
Vector-monitor effects for [**vector-display**](https://www.npmjs.com/package/@vector-display/core): phosphor trails that fade over time, bloom, and flicker, layered on top of [`@vector-display/webgl`](https://www.npmjs.com/package/@vector-display/webgl).
|
|
4
|
+
|
|
5
|
+
> **Status:** early (0.x). The API may change between minor versions.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @vector-display/core @vector-display/webgl @vector-display/beam-fx
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
ES modules only, with TypeScript declarations. Requires a browser with WebGL2.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { WebGLVectorRenderer } from "@vector-display/webgl";
|
|
19
|
+
import { PhosphorPipeline } from "@vector-display/beam-fx";
|
|
20
|
+
|
|
21
|
+
const renderer = WebGLVectorRenderer.fromCanvas(canvas, { width: 1024, height: 768 });
|
|
22
|
+
const phosphor = new PhosphorPipeline(renderer, { persistenceHalfLifeMilliseconds: 30, bloomStrength: 0.25 });
|
|
23
|
+
|
|
24
|
+
let previousTime = performance.now();
|
|
25
|
+
function drawFrame(time: number) {
|
|
26
|
+
// ...fill displayList for this frame...
|
|
27
|
+
phosphor.renderFrame(displayList, time - previousTime);
|
|
28
|
+
previousTime = time;
|
|
29
|
+
requestAnimationFrame(drawFrame);
|
|
30
|
+
}
|
|
31
|
+
requestAnimationFrame(drawFrame);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`renderFrame` replaces `renderer.clear()` + `renderer.drawDisplayList()`: it draws the beams, fades the previous frame, adds bloom, and writes the result to the canvas.
|
|
35
|
+
|
|
36
|
+
## Settings
|
|
37
|
+
|
|
38
|
+
Pass to the constructor or to `phosphor.setSettings({...})` at any time:
|
|
39
|
+
|
|
40
|
+
| Setting | Default | Effect |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `persistenceHalfLifeMilliseconds` | 40 | How long trails take to fade to half brightness |
|
|
43
|
+
| `bloomStrength` / `bloomBlurIterations` | 1.2 / 2 | Wide glow around bright lines (lower values look crisper) |
|
|
44
|
+
| `exposure` | 1.6 | Highlight rolloff |
|
|
45
|
+
| `flickerAmount` / `flickerFrequencyHz` | 0.08 / 15 | Smooth random brightness dips: depth (0–1) and how often the level changes |
|
|
46
|
+
|
|
47
|
+
Effects are time-based, so they look the same at 60 Hz and 120 Hz. Content that doesn't move keeps its true brightness; only movement leaves trails.
|
|
48
|
+
|
|
49
|
+
**Photosensitivity:** strong full-screen flicker around 3–30 Hz can trigger photosensitive seizures. Keep `flickerAmount` modest, or let users turn flicker off.
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT © Rick Segrest
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class FlickerGenerator {
|
|
2
|
+
private readonly randomSource;
|
|
3
|
+
private millisecondsIntoPeriod;
|
|
4
|
+
private startLevel;
|
|
5
|
+
private targetLevel;
|
|
6
|
+
constructor(randomSource?: () => number);
|
|
7
|
+
advance(elapsedMilliseconds: number, frequencyHz: number): number;
|
|
8
|
+
}
|
|
9
|
+
export default FlickerGenerator;
|
|
10
|
+
//# sourceMappingURL=FlickerGenerator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlickerGenerator.d.ts","sourceRoot":"","sources":["../src/FlickerGenerator.ts"],"names":[],"mappings":"AAEA,qBAAa,gBAAgB;IACzB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAS;IAE5B,YAAY,YAAY,GAAE,MAAM,MAAoB,EAInD;IAEM,OAAO,CAAC,mBAAmB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAYvE;CACJ;eAEc,gBAAgB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Smoothly interpolated random levels in [0, 1], changing target once per flicker period.
|
|
2
|
+
// Changes slower than the display refresh rate are visible; per-frame noise blurs into a steady image.
|
|
3
|
+
export class FlickerGenerator {
|
|
4
|
+
randomSource;
|
|
5
|
+
millisecondsIntoPeriod = 0;
|
|
6
|
+
startLevel;
|
|
7
|
+
targetLevel;
|
|
8
|
+
constructor(randomSource = Math.random) {
|
|
9
|
+
this.randomSource = randomSource;
|
|
10
|
+
this.startLevel = randomSource();
|
|
11
|
+
this.targetLevel = randomSource();
|
|
12
|
+
}
|
|
13
|
+
advance(elapsedMilliseconds, frequencyHz) {
|
|
14
|
+
if (frequencyHz <= 0)
|
|
15
|
+
return this.startLevel;
|
|
16
|
+
const periodMilliseconds = 1000 / frequencyHz;
|
|
17
|
+
this.millisecondsIntoPeriod += Math.max(elapsedMilliseconds, 0);
|
|
18
|
+
while (this.millisecondsIntoPeriod >= periodMilliseconds) {
|
|
19
|
+
this.millisecondsIntoPeriod -= periodMilliseconds;
|
|
20
|
+
this.startLevel = this.targetLevel;
|
|
21
|
+
this.targetLevel = this.randomSource();
|
|
22
|
+
}
|
|
23
|
+
const progress = this.millisecondsIntoPeriod / periodMilliseconds;
|
|
24
|
+
const easedProgress = progress * progress * (3 - 2 * progress);
|
|
25
|
+
return this.startLevel + (this.targetLevel - this.startLevel) * easedProgress;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export default FlickerGenerator;
|
|
29
|
+
//# sourceMappingURL=FlickerGenerator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlickerGenerator.js","sourceRoot":"","sources":["../src/FlickerGenerator.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,uGAAuG;AACvG,MAAM,OAAO,gBAAgB;IACR,YAAY,CAAe;IACpC,sBAAsB,GAAG,CAAC,CAAC;IAC3B,UAAU,CAAS;IACnB,WAAW,CAAS;IAE5B,YAAY,YAAY,GAAiB,IAAI,CAAC,MAAM;QAChD,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,UAAU,GAAG,YAAY,EAAE,CAAC;QACjC,IAAI,CAAC,WAAW,GAAG,YAAY,EAAE,CAAC;IACtC,CAAC;IAEM,OAAO,CAAC,mBAA2B,EAAE,WAAmB;QAC3D,IAAI,WAAW,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,UAAU,CAAC;QAC7C,MAAM,kBAAkB,GAAG,IAAI,GAAG,WAAW,CAAC;QAC9C,IAAI,CAAC,sBAAsB,IAAI,IAAI,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAC;QAChE,OAAO,IAAI,CAAC,sBAAsB,IAAI,kBAAkB,EAAE,CAAC;YACvD,IAAI,CAAC,sBAAsB,IAAI,kBAAkB,CAAC;YAClD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC;YACnC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QAC3C,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,sBAAsB,GAAG,kBAAkB,CAAC;QAClE,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC,UAAU,GAAG,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC;IAClF,CAAC;CACJ;AAED,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class FullscreenPass {
|
|
2
|
+
readonly program: WebGLProgram;
|
|
3
|
+
private readonly gl;
|
|
4
|
+
private readonly emptyVertexArray;
|
|
5
|
+
private readonly uniformLocations;
|
|
6
|
+
constructor(gl: WebGL2RenderingContext, fragmentSource: string);
|
|
7
|
+
use(): void;
|
|
8
|
+
getUniformLocation(uniformName: string): WebGLUniformLocation | null;
|
|
9
|
+
draw(): void;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
export default FullscreenPass;
|
|
13
|
+
//# sourceMappingURL=FullscreenPass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FullscreenPass.d.ts","sourceRoot":"","sources":["../src/FullscreenPass.ts"],"names":[],"mappings":"AAYA,qBAAa,cAAc;IACvB,SAAgB,OAAO,EAAE,YAAY,CAAC;IACtC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAyB;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyB;IAC1D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkD;IAEnF,YAAY,EAAE,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,EAI7D;IAEM,GAAG,IAAI,IAAI,CAEjB;IAEM,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAK1E;IAEM,IAAI,IAAI,IAAI,CAKlB;IAEM,OAAO,IAAI,IAAI,CAGrB;CACJ;eAEc,cAAc"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createShaderProgram } from "@vector-display/webgl";
|
|
2
|
+
// One oversized triangle generated from gl_VertexID, so no vertex buffer is needed.
|
|
3
|
+
const FULLSCREEN_VERTEX_SHADER = `#version 300 es
|
|
4
|
+
out vec2 v_textureCoordinate;
|
|
5
|
+
void main() {
|
|
6
|
+
vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
7
|
+
v_textureCoordinate = position;
|
|
8
|
+
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
|
9
|
+
}
|
|
10
|
+
`;
|
|
11
|
+
export class FullscreenPass {
|
|
12
|
+
program;
|
|
13
|
+
gl;
|
|
14
|
+
emptyVertexArray;
|
|
15
|
+
uniformLocations = new Map();
|
|
16
|
+
constructor(gl, fragmentSource) {
|
|
17
|
+
this.gl = gl;
|
|
18
|
+
this.program = createShaderProgram(gl, { vertexSource: FULLSCREEN_VERTEX_SHADER, fragmentSource });
|
|
19
|
+
this.emptyVertexArray = gl.createVertexArray();
|
|
20
|
+
}
|
|
21
|
+
use() {
|
|
22
|
+
this.gl.useProgram(this.program);
|
|
23
|
+
}
|
|
24
|
+
getUniformLocation(uniformName) {
|
|
25
|
+
if (!this.uniformLocations.has(uniformName)) {
|
|
26
|
+
this.uniformLocations.set(uniformName, this.gl.getUniformLocation(this.program, uniformName));
|
|
27
|
+
}
|
|
28
|
+
return this.uniformLocations.get(uniformName) ?? null;
|
|
29
|
+
}
|
|
30
|
+
draw() {
|
|
31
|
+
const gl = this.gl;
|
|
32
|
+
gl.bindVertexArray(this.emptyVertexArray);
|
|
33
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
34
|
+
gl.bindVertexArray(null);
|
|
35
|
+
}
|
|
36
|
+
dispose() {
|
|
37
|
+
this.gl.deleteVertexArray(this.emptyVertexArray);
|
|
38
|
+
this.gl.deleteProgram(this.program);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export default FullscreenPass;
|
|
42
|
+
//# sourceMappingURL=FullscreenPass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FullscreenPass.js","sourceRoot":"","sources":["../src/FullscreenPass.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,oFAAoF;AACpF,MAAM,wBAAwB,GAAG;;;;;;;CAOhC,CAAC;AAEF,MAAM,OAAO,cAAc;IACP,OAAO,CAAe;IACrB,EAAE,CAAyB;IAC3B,gBAAgB,CAAyB;IACzC,gBAAgB,GAAG,IAAI,GAAG,EAAuC,CAAC;IAEnF,YAAY,EAA0B,EAAE,cAAsB;QAC1D,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,OAAO,GAAG,mBAAmB,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,wBAAwB,EAAE,cAAc,EAAE,CAAC,CAAC;QACnG,IAAI,CAAC,gBAAgB,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAC;IACnD,CAAC;IAEM,GAAG;QACN,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IAEM,kBAAkB,CAAC,WAAmB;QACzC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;QAClG,CAAC;QACD,OAAO,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC;IAC1D,CAAC;IAEM,IAAI;QACP,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QACnB,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC1C,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAClC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAEM,OAAO;QACV,IAAI,CAAC,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACjD,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;CACJ;AAED,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DisplayList } from "@vector-display/core";
|
|
2
|
+
import type { WebGLVectorRenderer } from "@vector-display/webgl";
|
|
3
|
+
export interface PhosphorSettings {
|
|
4
|
+
readonly persistenceHalfLifeMilliseconds: number;
|
|
5
|
+
readonly bloomStrength: number;
|
|
6
|
+
readonly bloomBlurIterations: number;
|
|
7
|
+
readonly exposure: number;
|
|
8
|
+
readonly flickerAmount: number;
|
|
9
|
+
readonly flickerFrequencyHz: number;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_PHOSPHOR_SETTINGS: PhosphorSettings;
|
|
12
|
+
export declare class PhosphorPipeline {
|
|
13
|
+
private readonly gl;
|
|
14
|
+
private readonly renderer;
|
|
15
|
+
private readonly usesFloatStorage;
|
|
16
|
+
private readonly persistencePass;
|
|
17
|
+
private readonly blurPass;
|
|
18
|
+
private readonly compositePass;
|
|
19
|
+
private readonly flickerGenerator;
|
|
20
|
+
private settings;
|
|
21
|
+
private latestPhosphor;
|
|
22
|
+
private scratchPhosphor;
|
|
23
|
+
private currentBeams;
|
|
24
|
+
private bloomTargets;
|
|
25
|
+
constructor(renderer: WebGLVectorRenderer, settings?: Partial<PhosphorSettings>);
|
|
26
|
+
get isUsingFloatStorage(): boolean;
|
|
27
|
+
setSettings(settings: Partial<PhosphorSettings>): void;
|
|
28
|
+
renderFrame(displayList: DisplayList, elapsedMilliseconds: number): void;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
private resizeTargetsToDrawingBuffer;
|
|
31
|
+
private createTarget;
|
|
32
|
+
private drawBeamsIntoCurrentFrame;
|
|
33
|
+
private combineWithFadedPreviousFrame;
|
|
34
|
+
private blurIntoBloom;
|
|
35
|
+
private runBlurStep;
|
|
36
|
+
private calculateFlickerBrightness;
|
|
37
|
+
private compositeToCanvas;
|
|
38
|
+
private bindTexture;
|
|
39
|
+
private disposeTargets;
|
|
40
|
+
}
|
|
41
|
+
export default PhosphorPipeline;
|
|
42
|
+
//# sourceMappingURL=PhosphorPipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhosphorPipeline.d.ts","sourceRoot":"","sources":["../src/PhosphorPipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAMjE,MAAM,WAAW,gBAAgB;IAE7B,QAAQ,CAAC,+BAA+B,EAAE,MAAM,CAAC;IACjD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAG1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACvC;AAED,eAAO,MAAM,yBAAyB,EAAE,gBAOvC,CAAC;AAWF,qBAAa,gBAAgB;IACzB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAyB;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAC3C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAiB;IAC/C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA0B;IAC3D,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,eAAe,CAA6B;IACpD,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,YAAY,CAA6C;IAEjE,YAAY,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,GAAE,OAAO,CAAC,gBAAgB,CAAM,EAQlF;IAED,IAAW,mBAAmB,IAAI,OAAO,CAExC;IAEM,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAE5D;IAEM,WAAW,CAAC,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAO9E;IAEM,OAAO,IAAI,IAAI,CAKrB;IAED,OAAO,CAAC,4BAA4B;IAYpC,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,6BAA6B;IAoBrC,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,iBAAiB;IAmBzB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,cAAc;CAUzB;eAEc,gBAAgB"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { BLUR_FRAGMENT_SHADER, COMPOSITE_FRAGMENT_SHADER, PERSISTENCE_FRAGMENT_SHADER } from "./effectShaders.js";
|
|
2
|
+
import { FlickerGenerator } from "./FlickerGenerator.js";
|
|
3
|
+
import { FullscreenPass } from "./FullscreenPass.js";
|
|
4
|
+
import { RenderTarget } from "./RenderTarget.js";
|
|
5
|
+
export const DEFAULT_PHOSPHOR_SETTINGS = {
|
|
6
|
+
persistenceHalfLifeMilliseconds: 40,
|
|
7
|
+
bloomStrength: 1.2,
|
|
8
|
+
bloomBlurIterations: 2,
|
|
9
|
+
exposure: 1.6,
|
|
10
|
+
flickerAmount: 0.08,
|
|
11
|
+
flickerFrequencyHz: 15,
|
|
12
|
+
};
|
|
13
|
+
const MAXIMUM_FRAME_GAP_MILLISECONDS = 100;
|
|
14
|
+
// 8-bit targets can't store tiny values, so without a floor faint trails would never fully fade.
|
|
15
|
+
const EIGHT_BIT_DECAY_FLOOR = 1.5 / 255;
|
|
16
|
+
export class PhosphorPipeline {
|
|
17
|
+
gl;
|
|
18
|
+
renderer;
|
|
19
|
+
usesFloatStorage;
|
|
20
|
+
persistencePass;
|
|
21
|
+
blurPass;
|
|
22
|
+
compositePass;
|
|
23
|
+
flickerGenerator = new FlickerGenerator();
|
|
24
|
+
settings = DEFAULT_PHOSPHOR_SETTINGS;
|
|
25
|
+
latestPhosphor = null;
|
|
26
|
+
scratchPhosphor = null;
|
|
27
|
+
currentBeams = null;
|
|
28
|
+
bloomTargets = null;
|
|
29
|
+
constructor(renderer, settings = {}) {
|
|
30
|
+
this.renderer = renderer;
|
|
31
|
+
this.gl = renderer.gl;
|
|
32
|
+
this.usesFloatStorage = this.gl.getExtension("EXT_color_buffer_float") !== null;
|
|
33
|
+
this.persistencePass = new FullscreenPass(this.gl, PERSISTENCE_FRAGMENT_SHADER);
|
|
34
|
+
this.blurPass = new FullscreenPass(this.gl, BLUR_FRAGMENT_SHADER);
|
|
35
|
+
this.compositePass = new FullscreenPass(this.gl, COMPOSITE_FRAGMENT_SHADER);
|
|
36
|
+
this.setSettings(settings);
|
|
37
|
+
}
|
|
38
|
+
get isUsingFloatStorage() {
|
|
39
|
+
return this.usesFloatStorage;
|
|
40
|
+
}
|
|
41
|
+
setSettings(settings) {
|
|
42
|
+
this.settings = { ...this.settings, ...settings };
|
|
43
|
+
}
|
|
44
|
+
renderFrame(displayList, elapsedMilliseconds) {
|
|
45
|
+
const frameMilliseconds = Math.min(Math.max(elapsedMilliseconds, 0), MAXIMUM_FRAME_GAP_MILLISECONDS);
|
|
46
|
+
this.resizeTargetsToDrawingBuffer();
|
|
47
|
+
this.drawBeamsIntoCurrentFrame(displayList);
|
|
48
|
+
this.combineWithFadedPreviousFrame(frameMilliseconds);
|
|
49
|
+
this.blurIntoBloom();
|
|
50
|
+
this.compositeToCanvas(this.calculateFlickerBrightness(frameMilliseconds));
|
|
51
|
+
}
|
|
52
|
+
dispose() {
|
|
53
|
+
this.disposeTargets();
|
|
54
|
+
this.persistencePass.dispose();
|
|
55
|
+
this.blurPass.dispose();
|
|
56
|
+
this.compositePass.dispose();
|
|
57
|
+
}
|
|
58
|
+
resizeTargetsToDrawingBuffer() {
|
|
59
|
+
const width = this.gl.drawingBufferWidth;
|
|
60
|
+
const height = this.gl.drawingBufferHeight;
|
|
61
|
+
if (this.latestPhosphor?.width === width && this.latestPhosphor.height === height)
|
|
62
|
+
return;
|
|
63
|
+
this.disposeTargets();
|
|
64
|
+
this.latestPhosphor = this.createTarget({ width, height });
|
|
65
|
+
this.scratchPhosphor = this.createTarget({ width, height });
|
|
66
|
+
this.currentBeams = this.createTarget({ width, height });
|
|
67
|
+
const bloomSize = { width: Math.max(1, width >> 1), height: Math.max(1, height >> 1) };
|
|
68
|
+
this.bloomTargets = [this.createTarget(bloomSize), this.createTarget(bloomSize)];
|
|
69
|
+
}
|
|
70
|
+
createTarget(size) {
|
|
71
|
+
return new RenderTarget(this.gl, { ...size, usesFloatStorage: this.usesFloatStorage });
|
|
72
|
+
}
|
|
73
|
+
drawBeamsIntoCurrentFrame(displayList) {
|
|
74
|
+
const gl = this.gl;
|
|
75
|
+
this.currentBeams.bindForDrawing();
|
|
76
|
+
gl.clearColor(0, 0, 0, 1);
|
|
77
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
78
|
+
this.renderer.drawDisplayList(displayList);
|
|
79
|
+
}
|
|
80
|
+
// Keeps the brighter of this frame's beams and the faded trail. Adding them instead would make anything that
|
|
81
|
+
// stays still build up to 1 / (1 - decay) times its brightness, which grows with refresh rate and persistence.
|
|
82
|
+
combineWithFadedPreviousFrame(frameMilliseconds) {
|
|
83
|
+
const gl = this.gl;
|
|
84
|
+
const previous = this.latestPhosphor;
|
|
85
|
+
const next = this.scratchPhosphor;
|
|
86
|
+
const decay = Math.pow(0.5, frameMilliseconds / this.settings.persistenceHalfLifeMilliseconds);
|
|
87
|
+
next.bindForDrawing();
|
|
88
|
+
gl.disable(gl.BLEND);
|
|
89
|
+
this.persistencePass.use();
|
|
90
|
+
this.bindTexture(previous.texture, 0);
|
|
91
|
+
this.bindTexture(this.currentBeams.texture, 1);
|
|
92
|
+
gl.uniform1i(this.persistencePass.getUniformLocation("u_previousFrame"), 0);
|
|
93
|
+
gl.uniform1i(this.persistencePass.getUniformLocation("u_currentBeams"), 1);
|
|
94
|
+
gl.uniform1f(this.persistencePass.getUniformLocation("u_decay"), decay);
|
|
95
|
+
gl.uniform1f(this.persistencePass.getUniformLocation("u_decayFloor"), this.usesFloatStorage ? 0 : EIGHT_BIT_DECAY_FLOOR);
|
|
96
|
+
this.persistencePass.draw();
|
|
97
|
+
this.bindTexture(null, 1);
|
|
98
|
+
this.latestPhosphor = next;
|
|
99
|
+
this.scratchPhosphor = previous;
|
|
100
|
+
}
|
|
101
|
+
blurIntoBloom() {
|
|
102
|
+
const gl = this.gl;
|
|
103
|
+
const [horizontalTarget, verticalTarget] = this.bloomTargets;
|
|
104
|
+
gl.disable(gl.BLEND);
|
|
105
|
+
this.blurPass.use();
|
|
106
|
+
gl.uniform1i(this.blurPass.getUniformLocation("u_source"), 0);
|
|
107
|
+
let sourceTexture = this.latestPhosphor.texture;
|
|
108
|
+
for (let iteration = 0; iteration < this.settings.bloomBlurIterations; iteration++) {
|
|
109
|
+
this.runBlurStep(sourceTexture, { target: horizontalTarget, isHorizontal: true });
|
|
110
|
+
this.runBlurStep(horizontalTarget.texture, { target: verticalTarget, isHorizontal: false });
|
|
111
|
+
sourceTexture = verticalTarget.texture;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
runBlurStep(sourceTexture, step) {
|
|
115
|
+
const { target, isHorizontal } = step;
|
|
116
|
+
target.bindForDrawing();
|
|
117
|
+
this.bindTexture(sourceTexture, 0);
|
|
118
|
+
const texelStepX = isHorizontal ? 1 / target.width : 0;
|
|
119
|
+
const texelStepY = isHorizontal ? 0 : 1 / target.height;
|
|
120
|
+
this.gl.uniform2f(this.blurPass.getUniformLocation("u_texelStep"), texelStepX, texelStepY);
|
|
121
|
+
this.blurPass.draw();
|
|
122
|
+
}
|
|
123
|
+
calculateFlickerBrightness(frameMilliseconds) {
|
|
124
|
+
const flickerLevel = this.flickerGenerator.advance(frameMilliseconds, this.settings.flickerFrequencyHz);
|
|
125
|
+
const flickerDepth = Math.min(Math.max(this.settings.flickerAmount, 0), 1);
|
|
126
|
+
return 1 - flickerDepth * flickerLevel;
|
|
127
|
+
}
|
|
128
|
+
compositeToCanvas(flickerBrightness) {
|
|
129
|
+
const gl = this.gl;
|
|
130
|
+
const hasBloom = this.settings.bloomBlurIterations > 0;
|
|
131
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
132
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
133
|
+
gl.disable(gl.BLEND);
|
|
134
|
+
this.compositePass.use();
|
|
135
|
+
this.bindTexture(this.latestPhosphor.texture, 0);
|
|
136
|
+
this.bindTexture(this.bloomTargets[1].texture, 1);
|
|
137
|
+
gl.uniform1i(this.compositePass.getUniformLocation("u_phosphor"), 0);
|
|
138
|
+
gl.uniform1i(this.compositePass.getUniformLocation("u_bloom"), 1);
|
|
139
|
+
gl.uniform1f(this.compositePass.getUniformLocation("u_bloomStrength"), hasBloom ? this.settings.bloomStrength : 0);
|
|
140
|
+
gl.uniform1f(this.compositePass.getUniformLocation("u_exposure"), this.settings.exposure);
|
|
141
|
+
gl.uniform1f(this.compositePass.getUniformLocation("u_flickerBrightness"), flickerBrightness);
|
|
142
|
+
this.compositePass.draw();
|
|
143
|
+
this.bindTexture(null, 1);
|
|
144
|
+
this.bindTexture(null, 0);
|
|
145
|
+
}
|
|
146
|
+
bindTexture(texture, textureUnit) {
|
|
147
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit);
|
|
148
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
149
|
+
}
|
|
150
|
+
disposeTargets() {
|
|
151
|
+
this.latestPhosphor?.dispose();
|
|
152
|
+
this.scratchPhosphor?.dispose();
|
|
153
|
+
this.currentBeams?.dispose();
|
|
154
|
+
this.bloomTargets?.forEach((target) => target.dispose());
|
|
155
|
+
this.latestPhosphor = null;
|
|
156
|
+
this.scratchPhosphor = null;
|
|
157
|
+
this.currentBeams = null;
|
|
158
|
+
this.bloomTargets = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export default PhosphorPipeline;
|
|
162
|
+
//# sourceMappingURL=PhosphorPipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhosphorPipeline.js","sourceRoot":"","sources":["../src/PhosphorPipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface RenderTargetSpec {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly height: number;
|
|
4
|
+
readonly usesFloatStorage: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare class RenderTarget {
|
|
7
|
+
readonly texture: WebGLTexture;
|
|
8
|
+
readonly framebuffer: WebGLFramebuffer;
|
|
9
|
+
readonly width: number;
|
|
10
|
+
readonly height: number;
|
|
11
|
+
private readonly gl;
|
|
12
|
+
constructor(gl: WebGL2RenderingContext, spec: RenderTargetSpec);
|
|
13
|
+
bindForDrawing(): void;
|
|
14
|
+
dispose(): void;
|
|
15
|
+
}
|
|
16
|
+
export default RenderTarget;
|
|
17
|
+
//# sourceMappingURL=RenderTarget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RenderTarget.d.ts","sourceRoot":"","sources":["../src/RenderTarget.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACtC;AAED,qBAAa,YAAY;IACrB,SAAgB,OAAO,EAAE,YAAY,CAAC;IACtC,SAAgB,WAAW,EAAE,gBAAgB,CAAC;IAC9C,SAAgB,KAAK,EAAE,MAAM,CAAC;IAC9B,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAyB;IAE5C,YAAY,EAAE,EAAE,sBAAsB,EAAE,IAAI,EAAE,gBAAgB,EAS7D;IAEM,cAAc,IAAI,IAAI,CAG5B;IAEM,OAAO,IAAI,IAAI,CAGrB;CACJ;eAgBc,YAAY"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class RenderTarget {
|
|
2
|
+
texture;
|
|
3
|
+
framebuffer;
|
|
4
|
+
width;
|
|
5
|
+
height;
|
|
6
|
+
gl;
|
|
7
|
+
constructor(gl, spec) {
|
|
8
|
+
this.gl = gl;
|
|
9
|
+
this.width = spec.width;
|
|
10
|
+
this.height = spec.height;
|
|
11
|
+
this.texture = createTargetTexture(gl, spec);
|
|
12
|
+
this.framebuffer = gl.createFramebuffer();
|
|
13
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
|
14
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
|
|
15
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
16
|
+
}
|
|
17
|
+
bindForDrawing() {
|
|
18
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer);
|
|
19
|
+
this.gl.viewport(0, 0, this.width, this.height);
|
|
20
|
+
}
|
|
21
|
+
dispose() {
|
|
22
|
+
this.gl.deleteFramebuffer(this.framebuffer);
|
|
23
|
+
this.gl.deleteTexture(this.texture);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function createTargetTexture(gl, spec) {
|
|
27
|
+
const texture = gl.createTexture();
|
|
28
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
29
|
+
const internalFormat = spec.usesFloatStorage ? gl.RGBA16F : gl.RGBA8;
|
|
30
|
+
const dataType = spec.usesFloatStorage ? gl.HALF_FLOAT : gl.UNSIGNED_BYTE;
|
|
31
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, spec.width, spec.height, 0, gl.RGBA, dataType, null);
|
|
32
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
33
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
34
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
35
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
36
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
37
|
+
return texture;
|
|
38
|
+
}
|
|
39
|
+
export default RenderTarget;
|
|
40
|
+
//# sourceMappingURL=RenderTarget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RenderTarget.js","sourceRoot":"","sources":["../src/RenderTarget.ts"],"names":[],"mappings":"AAMA,MAAM,OAAO,YAAY;IACL,OAAO,CAAe;IACtB,WAAW,CAAmB;IAC9B,KAAK,CAAS;IACd,MAAM,CAAS;IACd,EAAE,CAAyB;IAE5C,YAAY,EAA0B,EAAE,IAAsB;QAC1D,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,mBAAmB,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC7C,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAC;QAC1C,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QACrD,EAAE,CAAC,oBAAoB,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,iBAAiB,EAAE,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QAC9F,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC;IAEM,cAAc;QACjB,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QAC/D,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACpD,CAAC;IAEM,OAAO;QACV,IAAI,CAAC,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC5C,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;CACJ;AAED,SAAS,mBAAmB,CAAC,EAA0B,EAAE,IAAsB;IAC3E,MAAM,OAAO,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC;IACnC,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACvC,MAAM,cAAc,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC;IACrE,MAAM,QAAQ,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC;IAC1E,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IACrG,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,kBAAkB,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAClE,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,kBAAkB,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAClE,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,cAAc,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC;IACrE,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,cAAc,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC;IACrE,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IACpC,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const PERSISTENCE_FRAGMENT_SHADER = "#version 300 es\nprecision highp float;\nuniform sampler2D u_previousFrame;\nuniform sampler2D u_currentBeams;\nuniform float u_decay;\nuniform float u_decayFloor;\nin vec2 v_textureCoordinate;\nout vec4 outColor;\nvoid main() {\n vec3 fadedTrail = max(texture(u_previousFrame, v_textureCoordinate).rgb * u_decay - u_decayFloor, 0.0);\n vec3 currentBeams = texture(u_currentBeams, v_textureCoordinate).rgb;\n outColor = vec4(max(fadedTrail, currentBeams), 1.0);\n}\n";
|
|
2
|
+
export declare const BLUR_FRAGMENT_SHADER = "#version 300 es\nprecision highp float;\nuniform sampler2D u_source;\nuniform vec2 u_texelStep;\nin vec2 v_textureCoordinate;\nout vec4 outColor;\nvoid main() {\n vec3 color = texture(u_source, v_textureCoordinate).rgb * 0.2270270270;\n color += texture(u_source, v_textureCoordinate + u_texelStep * 1.3846153846).rgb * 0.3162162162;\n color += texture(u_source, v_textureCoordinate - u_texelStep * 1.3846153846).rgb * 0.3162162162;\n color += texture(u_source, v_textureCoordinate + u_texelStep * 3.2307692308).rgb * 0.0702702703;\n color += texture(u_source, v_textureCoordinate - u_texelStep * 3.2307692308).rgb * 0.0702702703;\n outColor = vec4(color, 1.0);\n}\n";
|
|
3
|
+
export declare const COMPOSITE_FRAGMENT_SHADER = "#version 300 es\nprecision highp float;\nuniform sampler2D u_phosphor;\nuniform sampler2D u_bloom;\nuniform float u_bloomStrength;\nuniform float u_exposure;\nuniform float u_flickerBrightness;\nin vec2 v_textureCoordinate;\nout vec4 outColor;\nvoid main() {\n vec3 color = texture(u_phosphor, v_textureCoordinate).rgb\n + texture(u_bloom, v_textureCoordinate).rgb * u_bloomStrength;\n // Flicker scales the tone-mapped result, so its depth matches what the viewer sees.\n outColor = vec4((1.0 - exp(-color * u_exposure)) * u_flickerBrightness, 1.0);\n}\n";
|
|
4
|
+
//# sourceMappingURL=effectShaders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effectShaders.d.ts","sourceRoot":"","sources":["../src/effectShaders.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,2BAA2B,geAavC,CAAC;AAGF,eAAO,MAAM,oBAAoB,irBAchC,CAAC;AAEF,eAAO,MAAM,yBAAyB,gkBAerC,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const PERSISTENCE_FRAGMENT_SHADER = `#version 300 es
|
|
2
|
+
precision highp float;
|
|
3
|
+
uniform sampler2D u_previousFrame;
|
|
4
|
+
uniform sampler2D u_currentBeams;
|
|
5
|
+
uniform float u_decay;
|
|
6
|
+
uniform float u_decayFloor;
|
|
7
|
+
in vec2 v_textureCoordinate;
|
|
8
|
+
out vec4 outColor;
|
|
9
|
+
void main() {
|
|
10
|
+
vec3 fadedTrail = max(texture(u_previousFrame, v_textureCoordinate).rgb * u_decay - u_decayFloor, 0.0);
|
|
11
|
+
vec3 currentBeams = texture(u_currentBeams, v_textureCoordinate).rgb;
|
|
12
|
+
outColor = vec4(max(fadedTrail, currentBeams), 1.0);
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
// 9-tap Gaussian blur using 5 bilinear samples.
|
|
16
|
+
export const BLUR_FRAGMENT_SHADER = `#version 300 es
|
|
17
|
+
precision highp float;
|
|
18
|
+
uniform sampler2D u_source;
|
|
19
|
+
uniform vec2 u_texelStep;
|
|
20
|
+
in vec2 v_textureCoordinate;
|
|
21
|
+
out vec4 outColor;
|
|
22
|
+
void main() {
|
|
23
|
+
vec3 color = texture(u_source, v_textureCoordinate).rgb * 0.2270270270;
|
|
24
|
+
color += texture(u_source, v_textureCoordinate + u_texelStep * 1.3846153846).rgb * 0.3162162162;
|
|
25
|
+
color += texture(u_source, v_textureCoordinate - u_texelStep * 1.3846153846).rgb * 0.3162162162;
|
|
26
|
+
color += texture(u_source, v_textureCoordinate + u_texelStep * 3.2307692308).rgb * 0.0702702703;
|
|
27
|
+
color += texture(u_source, v_textureCoordinate - u_texelStep * 3.2307692308).rgb * 0.0702702703;
|
|
28
|
+
outColor = vec4(color, 1.0);
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
export const COMPOSITE_FRAGMENT_SHADER = `#version 300 es
|
|
32
|
+
precision highp float;
|
|
33
|
+
uniform sampler2D u_phosphor;
|
|
34
|
+
uniform sampler2D u_bloom;
|
|
35
|
+
uniform float u_bloomStrength;
|
|
36
|
+
uniform float u_exposure;
|
|
37
|
+
uniform float u_flickerBrightness;
|
|
38
|
+
in vec2 v_textureCoordinate;
|
|
39
|
+
out vec4 outColor;
|
|
40
|
+
void main() {
|
|
41
|
+
vec3 color = texture(u_phosphor, v_textureCoordinate).rgb
|
|
42
|
+
+ texture(u_bloom, v_textureCoordinate).rgb * u_bloomStrength;
|
|
43
|
+
// Flicker scales the tone-mapped result, so its depth matches what the viewer sees.
|
|
44
|
+
outColor = vec4((1.0 - exp(-color * u_exposure)) * u_flickerBrightness, 1.0);
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
//# sourceMappingURL=effectShaders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effectShaders.js","sourceRoot":"","sources":["../src/effectShaders.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,2BAA2B,GAAG;;;;;;;;;;;;;CAa1C,CAAC;AAEF,gDAAgD;AAChD,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;CAcnC,CAAC;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG;;;;;;;;;;;;;;;CAexC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AACpF,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vector-display/beam-fx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Phosphor persistence, bloom, and flicker effects for the vector-display WebGL renderer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Rick Segrest",
|
|
8
|
+
"email": "rsegrest77@gmail.com",
|
|
9
|
+
"url": "https://www.ricksegrest.com"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/rsegrest/vector-display.git",
|
|
14
|
+
"directory": "packages/beam-fx"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/rsegrest/vector-display/tree/main/packages/beam-fx#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/rsegrest/vector-display/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"@vector-display/source": "./src/index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"prepublishOnly": "tsc -b"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"!src/**/*.test.ts"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@vector-display/core": "0.1.0",
|
|
42
|
+
"@vector-display/webgl": "0.1.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Smoothly interpolated random levels in [0, 1], changing target once per flicker period.
|
|
2
|
+
// Changes slower than the display refresh rate are visible; per-frame noise blurs into a steady image.
|
|
3
|
+
export class FlickerGenerator {
|
|
4
|
+
private readonly randomSource: () => number;
|
|
5
|
+
private millisecondsIntoPeriod = 0;
|
|
6
|
+
private startLevel: number;
|
|
7
|
+
private targetLevel: number;
|
|
8
|
+
|
|
9
|
+
constructor(randomSource: () => number = Math.random) {
|
|
10
|
+
this.randomSource = randomSource;
|
|
11
|
+
this.startLevel = randomSource();
|
|
12
|
+
this.targetLevel = randomSource();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public advance(elapsedMilliseconds: number, frequencyHz: number): number {
|
|
16
|
+
if (frequencyHz <= 0) return this.startLevel;
|
|
17
|
+
const periodMilliseconds = 1000 / frequencyHz;
|
|
18
|
+
this.millisecondsIntoPeriod += Math.max(elapsedMilliseconds, 0);
|
|
19
|
+
while (this.millisecondsIntoPeriod >= periodMilliseconds) {
|
|
20
|
+
this.millisecondsIntoPeriod -= periodMilliseconds;
|
|
21
|
+
this.startLevel = this.targetLevel;
|
|
22
|
+
this.targetLevel = this.randomSource();
|
|
23
|
+
}
|
|
24
|
+
const progress = this.millisecondsIntoPeriod / periodMilliseconds;
|
|
25
|
+
const easedProgress = progress * progress * (3 - 2 * progress);
|
|
26
|
+
return this.startLevel + (this.targetLevel - this.startLevel) * easedProgress;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default FlickerGenerator;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createShaderProgram } from "@vector-display/webgl";
|
|
2
|
+
|
|
3
|
+
// One oversized triangle generated from gl_VertexID, so no vertex buffer is needed.
|
|
4
|
+
const FULLSCREEN_VERTEX_SHADER = `#version 300 es
|
|
5
|
+
out vec2 v_textureCoordinate;
|
|
6
|
+
void main() {
|
|
7
|
+
vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
8
|
+
v_textureCoordinate = position;
|
|
9
|
+
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
export class FullscreenPass {
|
|
14
|
+
public readonly program: WebGLProgram;
|
|
15
|
+
private readonly gl: WebGL2RenderingContext;
|
|
16
|
+
private readonly emptyVertexArray: WebGLVertexArrayObject;
|
|
17
|
+
private readonly uniformLocations = new Map<string, WebGLUniformLocation | null>();
|
|
18
|
+
|
|
19
|
+
constructor(gl: WebGL2RenderingContext, fragmentSource: string) {
|
|
20
|
+
this.gl = gl;
|
|
21
|
+
this.program = createShaderProgram(gl, { vertexSource: FULLSCREEN_VERTEX_SHADER, fragmentSource });
|
|
22
|
+
this.emptyVertexArray = gl.createVertexArray();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public use(): void {
|
|
26
|
+
this.gl.useProgram(this.program);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public getUniformLocation(uniformName: string): WebGLUniformLocation | null {
|
|
30
|
+
if (!this.uniformLocations.has(uniformName)) {
|
|
31
|
+
this.uniformLocations.set(uniformName, this.gl.getUniformLocation(this.program, uniformName));
|
|
32
|
+
}
|
|
33
|
+
return this.uniformLocations.get(uniformName) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public draw(): void {
|
|
37
|
+
const gl = this.gl;
|
|
38
|
+
gl.bindVertexArray(this.emptyVertexArray);
|
|
39
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
40
|
+
gl.bindVertexArray(null);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public dispose(): void {
|
|
44
|
+
this.gl.deleteVertexArray(this.emptyVertexArray);
|
|
45
|
+
this.gl.deleteProgram(this.program);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default FullscreenPass;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { DisplayList } from "@vector-display/core";
|
|
2
|
+
import type { WebGLVectorRenderer } from "@vector-display/webgl";
|
|
3
|
+
import { BLUR_FRAGMENT_SHADER, COMPOSITE_FRAGMENT_SHADER, PERSISTENCE_FRAGMENT_SHADER } from "./effectShaders.js";
|
|
4
|
+
import { FlickerGenerator } from "./FlickerGenerator.js";
|
|
5
|
+
import { FullscreenPass } from "./FullscreenPass.js";
|
|
6
|
+
import { RenderTarget } from "./RenderTarget.js";
|
|
7
|
+
|
|
8
|
+
export interface PhosphorSettings {
|
|
9
|
+
// Time for a trail to fade to half brightness.
|
|
10
|
+
readonly persistenceHalfLifeMilliseconds: number;
|
|
11
|
+
readonly bloomStrength: number;
|
|
12
|
+
readonly bloomBlurIterations: number;
|
|
13
|
+
readonly exposure: number;
|
|
14
|
+
// Deepest brightness dip as a fraction of the displayed image: 0 disables flicker, 0.3 dims by up to 30%.
|
|
15
|
+
// Strong full-screen flicker around 3–30 Hz can trigger photosensitive seizures; keep it subtle by default.
|
|
16
|
+
readonly flickerAmount: number;
|
|
17
|
+
// How often the flicker moves to a new random brightness level.
|
|
18
|
+
readonly flickerFrequencyHz: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_PHOSPHOR_SETTINGS: PhosphorSettings = {
|
|
22
|
+
persistenceHalfLifeMilliseconds: 40,
|
|
23
|
+
bloomStrength: 1.2,
|
|
24
|
+
bloomBlurIterations: 2,
|
|
25
|
+
exposure: 1.6,
|
|
26
|
+
flickerAmount: 0.08,
|
|
27
|
+
flickerFrequencyHz: 15,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const MAXIMUM_FRAME_GAP_MILLISECONDS = 100;
|
|
31
|
+
// 8-bit targets can't store tiny values, so without a floor faint trails would never fully fade.
|
|
32
|
+
const EIGHT_BIT_DECAY_FLOOR = 1.5 / 255;
|
|
33
|
+
|
|
34
|
+
interface TargetSize {
|
|
35
|
+
readonly width: number;
|
|
36
|
+
readonly height: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class PhosphorPipeline {
|
|
40
|
+
private readonly gl: WebGL2RenderingContext;
|
|
41
|
+
private readonly renderer: WebGLVectorRenderer;
|
|
42
|
+
private readonly usesFloatStorage: boolean;
|
|
43
|
+
private readonly persistencePass: FullscreenPass;
|
|
44
|
+
private readonly blurPass: FullscreenPass;
|
|
45
|
+
private readonly compositePass: FullscreenPass;
|
|
46
|
+
private readonly flickerGenerator = new FlickerGenerator();
|
|
47
|
+
private settings: PhosphorSettings = DEFAULT_PHOSPHOR_SETTINGS;
|
|
48
|
+
private latestPhosphor: RenderTarget | null = null;
|
|
49
|
+
private scratchPhosphor: RenderTarget | null = null;
|
|
50
|
+
private currentBeams: RenderTarget | null = null;
|
|
51
|
+
private bloomTargets: [RenderTarget, RenderTarget] | null = null;
|
|
52
|
+
|
|
53
|
+
constructor(renderer: WebGLVectorRenderer, settings: Partial<PhosphorSettings> = {}) {
|
|
54
|
+
this.renderer = renderer;
|
|
55
|
+
this.gl = renderer.gl;
|
|
56
|
+
this.usesFloatStorage = this.gl.getExtension("EXT_color_buffer_float") !== null;
|
|
57
|
+
this.persistencePass = new FullscreenPass(this.gl, PERSISTENCE_FRAGMENT_SHADER);
|
|
58
|
+
this.blurPass = new FullscreenPass(this.gl, BLUR_FRAGMENT_SHADER);
|
|
59
|
+
this.compositePass = new FullscreenPass(this.gl, COMPOSITE_FRAGMENT_SHADER);
|
|
60
|
+
this.setSettings(settings);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public get isUsingFloatStorage(): boolean {
|
|
64
|
+
return this.usesFloatStorage;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public setSettings(settings: Partial<PhosphorSettings>): void {
|
|
68
|
+
this.settings = { ...this.settings, ...settings };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public renderFrame(displayList: DisplayList, elapsedMilliseconds: number): void {
|
|
72
|
+
const frameMilliseconds = Math.min(Math.max(elapsedMilliseconds, 0), MAXIMUM_FRAME_GAP_MILLISECONDS);
|
|
73
|
+
this.resizeTargetsToDrawingBuffer();
|
|
74
|
+
this.drawBeamsIntoCurrentFrame(displayList);
|
|
75
|
+
this.combineWithFadedPreviousFrame(frameMilliseconds);
|
|
76
|
+
this.blurIntoBloom();
|
|
77
|
+
this.compositeToCanvas(this.calculateFlickerBrightness(frameMilliseconds));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public dispose(): void {
|
|
81
|
+
this.disposeTargets();
|
|
82
|
+
this.persistencePass.dispose();
|
|
83
|
+
this.blurPass.dispose();
|
|
84
|
+
this.compositePass.dispose();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private resizeTargetsToDrawingBuffer(): void {
|
|
88
|
+
const width = this.gl.drawingBufferWidth;
|
|
89
|
+
const height = this.gl.drawingBufferHeight;
|
|
90
|
+
if (this.latestPhosphor?.width === width && this.latestPhosphor.height === height) return;
|
|
91
|
+
this.disposeTargets();
|
|
92
|
+
this.latestPhosphor = this.createTarget({ width, height });
|
|
93
|
+
this.scratchPhosphor = this.createTarget({ width, height });
|
|
94
|
+
this.currentBeams = this.createTarget({ width, height });
|
|
95
|
+
const bloomSize = { width: Math.max(1, width >> 1), height: Math.max(1, height >> 1) };
|
|
96
|
+
this.bloomTargets = [this.createTarget(bloomSize), this.createTarget(bloomSize)];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private createTarget(size: TargetSize): RenderTarget {
|
|
100
|
+
return new RenderTarget(this.gl, { ...size, usesFloatStorage: this.usesFloatStorage });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private drawBeamsIntoCurrentFrame(displayList: DisplayList): void {
|
|
104
|
+
const gl = this.gl;
|
|
105
|
+
this.currentBeams!.bindForDrawing();
|
|
106
|
+
gl.clearColor(0, 0, 0, 1);
|
|
107
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
108
|
+
this.renderer.drawDisplayList(displayList);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Keeps the brighter of this frame's beams and the faded trail. Adding them instead would make anything that
|
|
112
|
+
// stays still build up to 1 / (1 - decay) times its brightness, which grows with refresh rate and persistence.
|
|
113
|
+
private combineWithFadedPreviousFrame(frameMilliseconds: number): void {
|
|
114
|
+
const gl = this.gl;
|
|
115
|
+
const previous = this.latestPhosphor!;
|
|
116
|
+
const next = this.scratchPhosphor!;
|
|
117
|
+
const decay = Math.pow(0.5, frameMilliseconds / this.settings.persistenceHalfLifeMilliseconds);
|
|
118
|
+
next.bindForDrawing();
|
|
119
|
+
gl.disable(gl.BLEND);
|
|
120
|
+
this.persistencePass.use();
|
|
121
|
+
this.bindTexture(previous.texture, 0);
|
|
122
|
+
this.bindTexture(this.currentBeams!.texture, 1);
|
|
123
|
+
gl.uniform1i(this.persistencePass.getUniformLocation("u_previousFrame"), 0);
|
|
124
|
+
gl.uniform1i(this.persistencePass.getUniformLocation("u_currentBeams"), 1);
|
|
125
|
+
gl.uniform1f(this.persistencePass.getUniformLocation("u_decay"), decay);
|
|
126
|
+
gl.uniform1f(this.persistencePass.getUniformLocation("u_decayFloor"), this.usesFloatStorage ? 0 : EIGHT_BIT_DECAY_FLOOR);
|
|
127
|
+
this.persistencePass.draw();
|
|
128
|
+
this.bindTexture(null, 1);
|
|
129
|
+
this.latestPhosphor = next;
|
|
130
|
+
this.scratchPhosphor = previous;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private blurIntoBloom(): void {
|
|
134
|
+
const gl = this.gl;
|
|
135
|
+
const [horizontalTarget, verticalTarget] = this.bloomTargets!;
|
|
136
|
+
gl.disable(gl.BLEND);
|
|
137
|
+
this.blurPass.use();
|
|
138
|
+
gl.uniform1i(this.blurPass.getUniformLocation("u_source"), 0);
|
|
139
|
+
let sourceTexture = this.latestPhosphor!.texture;
|
|
140
|
+
for (let iteration = 0; iteration < this.settings.bloomBlurIterations; iteration++) {
|
|
141
|
+
this.runBlurStep(sourceTexture, { target: horizontalTarget, isHorizontal: true });
|
|
142
|
+
this.runBlurStep(horizontalTarget.texture, { target: verticalTarget, isHorizontal: false });
|
|
143
|
+
sourceTexture = verticalTarget.texture;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private runBlurStep(sourceTexture: WebGLTexture, step: { target: RenderTarget; isHorizontal: boolean }): void {
|
|
148
|
+
const { target, isHorizontal } = step;
|
|
149
|
+
target.bindForDrawing();
|
|
150
|
+
this.bindTexture(sourceTexture, 0);
|
|
151
|
+
const texelStepX = isHorizontal ? 1 / target.width : 0;
|
|
152
|
+
const texelStepY = isHorizontal ? 0 : 1 / target.height;
|
|
153
|
+
this.gl.uniform2f(this.blurPass.getUniformLocation("u_texelStep"), texelStepX, texelStepY);
|
|
154
|
+
this.blurPass.draw();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private calculateFlickerBrightness(frameMilliseconds: number): number {
|
|
158
|
+
const flickerLevel = this.flickerGenerator.advance(frameMilliseconds, this.settings.flickerFrequencyHz);
|
|
159
|
+
const flickerDepth = Math.min(Math.max(this.settings.flickerAmount, 0), 1);
|
|
160
|
+
return 1 - flickerDepth * flickerLevel;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private compositeToCanvas(flickerBrightness: number): void {
|
|
164
|
+
const gl = this.gl;
|
|
165
|
+
const hasBloom = this.settings.bloomBlurIterations > 0;
|
|
166
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
167
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
168
|
+
gl.disable(gl.BLEND);
|
|
169
|
+
this.compositePass.use();
|
|
170
|
+
this.bindTexture(this.latestPhosphor!.texture, 0);
|
|
171
|
+
this.bindTexture(this.bloomTargets![1].texture, 1);
|
|
172
|
+
gl.uniform1i(this.compositePass.getUniformLocation("u_phosphor"), 0);
|
|
173
|
+
gl.uniform1i(this.compositePass.getUniformLocation("u_bloom"), 1);
|
|
174
|
+
gl.uniform1f(this.compositePass.getUniformLocation("u_bloomStrength"), hasBloom ? this.settings.bloomStrength : 0);
|
|
175
|
+
gl.uniform1f(this.compositePass.getUniformLocation("u_exposure"), this.settings.exposure);
|
|
176
|
+
gl.uniform1f(this.compositePass.getUniformLocation("u_flickerBrightness"), flickerBrightness);
|
|
177
|
+
this.compositePass.draw();
|
|
178
|
+
this.bindTexture(null, 1);
|
|
179
|
+
this.bindTexture(null, 0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private bindTexture(texture: WebGLTexture | null, textureUnit: number): void {
|
|
183
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit);
|
|
184
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private disposeTargets(): void {
|
|
188
|
+
this.latestPhosphor?.dispose();
|
|
189
|
+
this.scratchPhosphor?.dispose();
|
|
190
|
+
this.currentBeams?.dispose();
|
|
191
|
+
this.bloomTargets?.forEach((target) => target.dispose());
|
|
192
|
+
this.latestPhosphor = null;
|
|
193
|
+
this.scratchPhosphor = null;
|
|
194
|
+
this.currentBeams = null;
|
|
195
|
+
this.bloomTargets = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default PhosphorPipeline;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface RenderTargetSpec {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly height: number;
|
|
4
|
+
readonly usesFloatStorage: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class RenderTarget {
|
|
8
|
+
public readonly texture: WebGLTexture;
|
|
9
|
+
public readonly framebuffer: WebGLFramebuffer;
|
|
10
|
+
public readonly width: number;
|
|
11
|
+
public readonly height: number;
|
|
12
|
+
private readonly gl: WebGL2RenderingContext;
|
|
13
|
+
|
|
14
|
+
constructor(gl: WebGL2RenderingContext, spec: RenderTargetSpec) {
|
|
15
|
+
this.gl = gl;
|
|
16
|
+
this.width = spec.width;
|
|
17
|
+
this.height = spec.height;
|
|
18
|
+
this.texture = createTargetTexture(gl, spec);
|
|
19
|
+
this.framebuffer = gl.createFramebuffer();
|
|
20
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
|
21
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
|
|
22
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public bindForDrawing(): void {
|
|
26
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer);
|
|
27
|
+
this.gl.viewport(0, 0, this.width, this.height);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public dispose(): void {
|
|
31
|
+
this.gl.deleteFramebuffer(this.framebuffer);
|
|
32
|
+
this.gl.deleteTexture(this.texture);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createTargetTexture(gl: WebGL2RenderingContext, spec: RenderTargetSpec): WebGLTexture {
|
|
37
|
+
const texture = gl.createTexture();
|
|
38
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
39
|
+
const internalFormat = spec.usesFloatStorage ? gl.RGBA16F : gl.RGBA8;
|
|
40
|
+
const dataType = spec.usesFloatStorage ? gl.HALF_FLOAT : gl.UNSIGNED_BYTE;
|
|
41
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, spec.width, spec.height, 0, gl.RGBA, dataType, null);
|
|
42
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
43
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
44
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
45
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
46
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
47
|
+
return texture;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default RenderTarget;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const PERSISTENCE_FRAGMENT_SHADER = `#version 300 es
|
|
2
|
+
precision highp float;
|
|
3
|
+
uniform sampler2D u_previousFrame;
|
|
4
|
+
uniform sampler2D u_currentBeams;
|
|
5
|
+
uniform float u_decay;
|
|
6
|
+
uniform float u_decayFloor;
|
|
7
|
+
in vec2 v_textureCoordinate;
|
|
8
|
+
out vec4 outColor;
|
|
9
|
+
void main() {
|
|
10
|
+
vec3 fadedTrail = max(texture(u_previousFrame, v_textureCoordinate).rgb * u_decay - u_decayFloor, 0.0);
|
|
11
|
+
vec3 currentBeams = texture(u_currentBeams, v_textureCoordinate).rgb;
|
|
12
|
+
outColor = vec4(max(fadedTrail, currentBeams), 1.0);
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
// 9-tap Gaussian blur using 5 bilinear samples.
|
|
17
|
+
export const BLUR_FRAGMENT_SHADER = `#version 300 es
|
|
18
|
+
precision highp float;
|
|
19
|
+
uniform sampler2D u_source;
|
|
20
|
+
uniform vec2 u_texelStep;
|
|
21
|
+
in vec2 v_textureCoordinate;
|
|
22
|
+
out vec4 outColor;
|
|
23
|
+
void main() {
|
|
24
|
+
vec3 color = texture(u_source, v_textureCoordinate).rgb * 0.2270270270;
|
|
25
|
+
color += texture(u_source, v_textureCoordinate + u_texelStep * 1.3846153846).rgb * 0.3162162162;
|
|
26
|
+
color += texture(u_source, v_textureCoordinate - u_texelStep * 1.3846153846).rgb * 0.3162162162;
|
|
27
|
+
color += texture(u_source, v_textureCoordinate + u_texelStep * 3.2307692308).rgb * 0.0702702703;
|
|
28
|
+
color += texture(u_source, v_textureCoordinate - u_texelStep * 3.2307692308).rgb * 0.0702702703;
|
|
29
|
+
outColor = vec4(color, 1.0);
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export const COMPOSITE_FRAGMENT_SHADER = `#version 300 es
|
|
34
|
+
precision highp float;
|
|
35
|
+
uniform sampler2D u_phosphor;
|
|
36
|
+
uniform sampler2D u_bloom;
|
|
37
|
+
uniform float u_bloomStrength;
|
|
38
|
+
uniform float u_exposure;
|
|
39
|
+
uniform float u_flickerBrightness;
|
|
40
|
+
in vec2 v_textureCoordinate;
|
|
41
|
+
out vec4 outColor;
|
|
42
|
+
void main() {
|
|
43
|
+
vec3 color = texture(u_phosphor, v_textureCoordinate).rgb
|
|
44
|
+
+ texture(u_bloom, v_textureCoordinate).rgb * u_bloomStrength;
|
|
45
|
+
// Flicker scales the tone-mapped result, so its depth matches what the viewer sees.
|
|
46
|
+
outColor = vec4((1.0 - exp(-color * u_exposure)) * u_flickerBrightness, 1.0);
|
|
47
|
+
}
|
|
48
|
+
`;
|
package/src/index.ts
ADDED