@brochington/shader-backgrounds 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/.github/workflows/deploy-demo-to-pages.yml +63 -0
- package/PLUGINS.md +269 -0
- package/README.md +159 -0
- package/demo.js +1044 -0
- package/index.html +194 -0
- package/package.json +23 -0
- package/src/index.ts +5 -0
- package/src/lib/components/web-component.ts +198 -0
- package/src/lib/core/ShaderCanvas.ts +235 -0
- package/src/lib/core/types.ts +26 -0
- package/src/lib/plugins/AuroraWavesPlugin.ts +128 -0
- package/src/lib/plugins/CausticsPlugin.ts +128 -0
- package/src/lib/plugins/ContourLinesPlugin.ts +148 -0
- package/src/lib/plugins/DreamyBokehPlugin.ts +191 -0
- package/src/lib/plugins/GradientPlugin.ts +445 -0
- package/src/lib/plugins/GrainyFogPlugin.ts +139 -0
- package/src/lib/plugins/InkWashPlugin.ts +182 -0
- package/src/lib/plugins/LiquidOrbPlugin.ts +140 -0
- package/src/lib/plugins/RetroGridPlugin.ts +77 -0
- package/src/lib/plugins/SoftStarfieldPlugin.ts +156 -0
- package/src/lib/plugins/StainedGlassPlugin.ts +261 -0
- package/src/lib/plugins/index.ts +11 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +19 -0
- package/vite.demo.config.ts +13 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Color } from 'ogl';
|
|
2
|
+
import { ShaderPlugin } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export type InkWashConfig = {
|
|
5
|
+
/** Paper/background color */
|
|
6
|
+
paperColor: string;
|
|
7
|
+
/** Ink/pigment color */
|
|
8
|
+
inkColor: string;
|
|
9
|
+
|
|
10
|
+
/** Overall pattern scale */
|
|
11
|
+
scale?: number; // default 1.4
|
|
12
|
+
/** Motion speed */
|
|
13
|
+
speed?: number; // default 0.18
|
|
14
|
+
/** Flow / warping amount */
|
|
15
|
+
flow?: number; // default 0.85
|
|
16
|
+
/** Contrast / punch of ink */
|
|
17
|
+
contrast?: number; // default 1.15
|
|
18
|
+
/** Granulation (pigment clumping) */
|
|
19
|
+
granulation?: number; // default 0.35
|
|
20
|
+
/** Vignette (0..1) */
|
|
21
|
+
vignette?: number; // default 0.35
|
|
22
|
+
/** Grain strength */
|
|
23
|
+
grainAmount?: number; // default 0.03
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class InkWashPlugin implements ShaderPlugin {
|
|
27
|
+
name = 'ink-wash';
|
|
28
|
+
|
|
29
|
+
fragmentShader = /* glsl */ `
|
|
30
|
+
precision highp float;
|
|
31
|
+
#ifdef GL_OES_standard_derivatives
|
|
32
|
+
#extension GL_OES_standard_derivatives : enable
|
|
33
|
+
#endif
|
|
34
|
+
|
|
35
|
+
uniform float uTimeInternal;
|
|
36
|
+
uniform vec2 uResolution;
|
|
37
|
+
uniform vec3 uPaper;
|
|
38
|
+
uniform vec3 uInk;
|
|
39
|
+
uniform float uScale;
|
|
40
|
+
uniform float uFlow;
|
|
41
|
+
uniform float uContrast;
|
|
42
|
+
uniform float uGran;
|
|
43
|
+
uniform float uVignette;
|
|
44
|
+
uniform float uGrain;
|
|
45
|
+
|
|
46
|
+
varying vec2 vUv;
|
|
47
|
+
|
|
48
|
+
float hash12(vec2 p) {
|
|
49
|
+
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
50
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
51
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
float noise(vec2 p) {
|
|
55
|
+
vec2 i = floor(p);
|
|
56
|
+
vec2 f = fract(p);
|
|
57
|
+
float a = hash12(i);
|
|
58
|
+
float b = hash12(i + vec2(1.0, 0.0));
|
|
59
|
+
float c = hash12(i + vec2(0.0, 1.0));
|
|
60
|
+
float d = hash12(i + vec2(1.0, 1.0));
|
|
61
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
62
|
+
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
float fbm(vec2 p) {
|
|
66
|
+
float v = 0.0;
|
|
67
|
+
float a = 0.5;
|
|
68
|
+
mat2 rot = mat2(0.80, -0.60, 0.60, 0.80);
|
|
69
|
+
for (int i = 0; i < 6; i++) {
|
|
70
|
+
v += a * noise(p);
|
|
71
|
+
p = rot * p * 2.02 + 17.17;
|
|
72
|
+
a *= 0.55;
|
|
73
|
+
}
|
|
74
|
+
return v;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
vec2 flowField(vec2 p, float t) {
|
|
78
|
+
// Curl-ish flow from two fbm samples
|
|
79
|
+
float n1 = fbm(p * 1.10 + vec2(0.0, t * 0.12));
|
|
80
|
+
float n2 = fbm(p * 1.10 + vec2(13.1, -t * 0.10));
|
|
81
|
+
vec2 f = vec2(n1 - 0.5, n2 - 0.5);
|
|
82
|
+
return f;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
float aawidth(float x) {
|
|
86
|
+
#ifdef GL_OES_standard_derivatives
|
|
87
|
+
return fwidth(x);
|
|
88
|
+
#else
|
|
89
|
+
return 1.0 / max(1.0, min(uResolution.x, uResolution.y));
|
|
90
|
+
#endif
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
void main() {
|
|
94
|
+
float aspect = uResolution.x / uResolution.y;
|
|
95
|
+
vec2 uv = vUv;
|
|
96
|
+
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
|
|
97
|
+
float t = uTimeInternal;
|
|
98
|
+
|
|
99
|
+
// Base paper tone + subtle fiber texture
|
|
100
|
+
float fiber = noise(uv * vec2(uResolution.x, uResolution.y) * 0.0025);
|
|
101
|
+
float fiber2 = noise(uv * vec2(uResolution.x, uResolution.y) * 0.0060 + 19.19);
|
|
102
|
+
vec3 col = uPaper + (fiber - 0.5) * 0.05 + (fiber2 - 0.5) * 0.03;
|
|
103
|
+
|
|
104
|
+
// Domain-warped pigment field
|
|
105
|
+
vec2 q = p * uScale;
|
|
106
|
+
vec2 f = flowField(q, t) * (0.55 * uFlow);
|
|
107
|
+
vec2 r = flowField(q + f * 1.35, t + 7.7) * (0.55 * uFlow);
|
|
108
|
+
vec2 w = f + r;
|
|
109
|
+
|
|
110
|
+
float base = fbm(q + w);
|
|
111
|
+
float detail = fbm(q * 2.10 - w * 0.70 + vec2(-0.07 * t, 0.05 * t));
|
|
112
|
+
float field = base * 0.72 + detail * 0.35;
|
|
113
|
+
|
|
114
|
+
// Contrast shaping
|
|
115
|
+
field = clamp(field, 0.0, 1.0);
|
|
116
|
+
field = pow(field, 1.0 / max(0.001, uContrast));
|
|
117
|
+
|
|
118
|
+
// Pigment coverage (soft threshold)
|
|
119
|
+
float edgeW = 0.08 + 0.06 * (1.0 - uFlow);
|
|
120
|
+
float ink = smoothstep(0.38 - edgeW, 0.68 + edgeW, field);
|
|
121
|
+
|
|
122
|
+
// “Tide lines”: emphasize places where the field changes quickly
|
|
123
|
+
float grad = 0.0;
|
|
124
|
+
#ifdef GL_OES_standard_derivatives
|
|
125
|
+
grad = (abs(dFdx(field)) + abs(dFdy(field))) * 6.0;
|
|
126
|
+
#else
|
|
127
|
+
grad = aawidth(field) * 120.0;
|
|
128
|
+
#endif
|
|
129
|
+
float tide = smoothstep(0.20, 0.95, grad);
|
|
130
|
+
tide *= (1.0 - ink) * 0.55 + ink * 0.25; // strongest near transitions
|
|
131
|
+
|
|
132
|
+
// Pigment granulation: high-frequency noise visible where ink exists
|
|
133
|
+
float gran = noise(q * 10.0 + vec2(31.2, 17.8)) - 0.5;
|
|
134
|
+
float gran2 = noise(q * 18.0 + vec2(9.7, 53.1)) - 0.5;
|
|
135
|
+
float granTex = gran * 0.8 + gran2 * 0.6;
|
|
136
|
+
float granAmt = uGran * (0.35 + 0.65 * ink);
|
|
137
|
+
|
|
138
|
+
float pigment = clamp(ink + tide * 0.35 + granTex * granAmt, 0.0, 1.0);
|
|
139
|
+
|
|
140
|
+
// Mix ink into paper
|
|
141
|
+
col = mix(col, mix(col, uInk, 0.92), pigment);
|
|
142
|
+
|
|
143
|
+
// Vignette
|
|
144
|
+
float v = 1.0 - smoothstep(0.25, 1.15, length(p * vec2(1.0, 0.95)));
|
|
145
|
+
col *= mix(1.0, v, clamp(uVignette, 0.0, 1.0));
|
|
146
|
+
|
|
147
|
+
// Grain
|
|
148
|
+
float g = (hash12(uv * uResolution + t * 61.0) - 0.5) * 2.0;
|
|
149
|
+
col += g * uGrain;
|
|
150
|
+
|
|
151
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
uniforms: any;
|
|
156
|
+
private speed: number;
|
|
157
|
+
|
|
158
|
+
constructor(config: InkWashConfig) {
|
|
159
|
+
const paper = new Color(config.paperColor);
|
|
160
|
+
const ink = new Color(config.inkColor);
|
|
161
|
+
|
|
162
|
+
this.speed = config.speed ?? 0.18;
|
|
163
|
+
|
|
164
|
+
this.uniforms = {
|
|
165
|
+
uPaper: { value: [paper.r, paper.g, paper.b] },
|
|
166
|
+
uInk: { value: [ink.r, ink.g, ink.b] },
|
|
167
|
+
uScale: { value: config.scale ?? 1.4 },
|
|
168
|
+
uFlow: { value: config.flow ?? 0.85 },
|
|
169
|
+
uContrast: { value: config.contrast ?? 1.15 },
|
|
170
|
+
uGran: { value: config.granulation ?? 0.35 },
|
|
171
|
+
uVignette: { value: config.vignette ?? 0.35 },
|
|
172
|
+
uGrain: { value: config.grainAmount ?? 0.03 },
|
|
173
|
+
uTimeInternal: { value: 0 },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
onRender(dt: number) {
|
|
178
|
+
this.uniforms.uTimeInternal.value += dt * 0.001 * this.speed;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Color } from 'ogl';
|
|
2
|
+
import { ShaderPlugin } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export type LiquidOrbConfig = {
|
|
5
|
+
color: string;
|
|
6
|
+
backgroundColor: string;
|
|
7
|
+
count?: number; // Number of blobs, max 20
|
|
8
|
+
speed?: number;
|
|
9
|
+
gooeyness?: number; // Smooth-min blending factor, default 0.3
|
|
10
|
+
edgeSoftness?: number; // Edge AA/softness, default 0.02
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type Orb = {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
vx: number;
|
|
17
|
+
vy: number;
|
|
18
|
+
radius: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class LiquidOrbPlugin implements ShaderPlugin {
|
|
22
|
+
name = 'liquid-orb';
|
|
23
|
+
private static MAX_ORBS = 20;
|
|
24
|
+
|
|
25
|
+
fragmentShader = /* glsl */ `
|
|
26
|
+
precision highp float;
|
|
27
|
+
uniform vec2 uResolution;
|
|
28
|
+
uniform vec3 uColor;
|
|
29
|
+
uniform vec3 uBgColor;
|
|
30
|
+
uniform float uGooeyness;
|
|
31
|
+
uniform float uEdgeSoftness;
|
|
32
|
+
|
|
33
|
+
uniform int uCount;
|
|
34
|
+
uniform vec3 uOrbs[${LiquidOrbPlugin.MAX_ORBS}]; // x, y, radius
|
|
35
|
+
|
|
36
|
+
varying vec2 vUv;
|
|
37
|
+
|
|
38
|
+
// Smooth Minimum function (The "Goo" math)
|
|
39
|
+
float smin(float a, float b, float k) {
|
|
40
|
+
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
|
41
|
+
return mix(b, a, h) - k * h * (1.0 - h);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void main() {
|
|
45
|
+
float aspect = uResolution.x / uResolution.y;
|
|
46
|
+
vec2 uv = vUv * 2.0 - 1.0;
|
|
47
|
+
uv.x *= aspect;
|
|
48
|
+
|
|
49
|
+
// Calculate Signed Distance Field (SDF)
|
|
50
|
+
// Start with a large distance
|
|
51
|
+
float d = 100.0;
|
|
52
|
+
|
|
53
|
+
for (int i = 0; i < ${LiquidOrbPlugin.MAX_ORBS}; i++) {
|
|
54
|
+
if (i >= uCount) break;
|
|
55
|
+
|
|
56
|
+
vec3 orb = uOrbs[i];
|
|
57
|
+
vec2 pos = orb.xy;
|
|
58
|
+
pos.x *= aspect; // Correct aspect for orb position too
|
|
59
|
+
|
|
60
|
+
float radius = orb.z;
|
|
61
|
+
|
|
62
|
+
// Distance from pixel to orb center minus radius
|
|
63
|
+
float dist = length(uv - pos) - radius;
|
|
64
|
+
|
|
65
|
+
// Smoothly blend distances
|
|
66
|
+
d = smin(d, dist, uGooeyness);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Render based on threshold
|
|
70
|
+
// If d < 0.0, we are inside the goo
|
|
71
|
+
// We use smoothstep for antialiasing the edge
|
|
72
|
+
float alpha = 1.0 - smoothstep(0.0, max(0.0001, uEdgeSoftness), d);
|
|
73
|
+
|
|
74
|
+
vec3 color = mix(uBgColor, uColor, alpha);
|
|
75
|
+
|
|
76
|
+
gl_FragColor = vec4(color, 1.0);
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
uniforms: any;
|
|
81
|
+
private orbs: Orb[] = [];
|
|
82
|
+
private orbData: Array<[number, number, number]> = [];
|
|
83
|
+
private speedMultiplier: number;
|
|
84
|
+
|
|
85
|
+
constructor(config: LiquidOrbConfig) {
|
|
86
|
+
const fg = new Color(config.color);
|
|
87
|
+
const bg = new Color(config.backgroundColor);
|
|
88
|
+
const count = Math.min(config.count ?? 5, LiquidOrbPlugin.MAX_ORBS);
|
|
89
|
+
this.speedMultiplier = config.speed ?? 0.5;
|
|
90
|
+
|
|
91
|
+
// Initialize Physics
|
|
92
|
+
for (let i = 0; i < count; i++) {
|
|
93
|
+
this.orbs.push({
|
|
94
|
+
x: (Math.random() * 2 - 1) * 0.8,
|
|
95
|
+
y: (Math.random() * 2 - 1) * 0.8,
|
|
96
|
+
vx: (Math.random() - 0.5) * 0.01,
|
|
97
|
+
vy: (Math.random() - 0.5) * 0.01,
|
|
98
|
+
radius: 0.2 + Math.random() * 0.2,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// IMPORTANT: OGL expects uniform vec3[] values as an array of vec3-ish triplets,
|
|
103
|
+
// not a packed Float32Array. If we pass a packed typed array, it will not map
|
|
104
|
+
// correctly to `uniform vec3 uOrbs[MAX]` and you'll effectively see zeros.
|
|
105
|
+
this.orbData = Array.from({ length: LiquidOrbPlugin.MAX_ORBS }, () => [0, 0, 0]);
|
|
106
|
+
|
|
107
|
+
this.uniforms = {
|
|
108
|
+
uColor: { value: [fg.r, fg.g, fg.b] },
|
|
109
|
+
uBgColor: { value: [bg.r, bg.g, bg.b] },
|
|
110
|
+
uCount: { value: count },
|
|
111
|
+
uOrbs: { value: this.orbData },
|
|
112
|
+
uGooeyness: { value: config.gooeyness ?? 0.3 },
|
|
113
|
+
uEdgeSoftness: { value: config.edgeSoftness ?? 0.02 },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onRender(dt: number) {
|
|
118
|
+
// Physics Step
|
|
119
|
+
// dt is usually ~16ms.
|
|
120
|
+
|
|
121
|
+
let i = 0;
|
|
122
|
+
for (const orb of this.orbs) {
|
|
123
|
+
orb.x += orb.vx * this.speedMultiplier * (dt / 16);
|
|
124
|
+
orb.y += orb.vy * this.speedMultiplier * (dt / 16);
|
|
125
|
+
|
|
126
|
+
// Bounce off walls
|
|
127
|
+
if (orb.x < -1.0 || orb.x > 1.0) orb.vx *= -1;
|
|
128
|
+
if (orb.y < -1.0 || orb.y > 1.0) orb.vy *= -1;
|
|
129
|
+
|
|
130
|
+
// Update vec3[] triplets
|
|
131
|
+
const v = this.orbData[i];
|
|
132
|
+
v[0] = orb.x;
|
|
133
|
+
v[1] = orb.y;
|
|
134
|
+
v[2] = orb.radius;
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// No need to reassign; values are updated in-place.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Color } from 'ogl';
|
|
2
|
+
import { ShaderPlugin } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export type RetroGridConfig = {
|
|
5
|
+
gridColor: string;
|
|
6
|
+
backgroundColor: string;
|
|
7
|
+
speed?: number; // default 1.0
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class RetroGridPlugin implements ShaderPlugin {
|
|
11
|
+
name = 'retro-grid';
|
|
12
|
+
|
|
13
|
+
fragmentShader = /* glsl */ `
|
|
14
|
+
precision highp float;
|
|
15
|
+
uniform float uTime;
|
|
16
|
+
uniform vec3 uGridColor;
|
|
17
|
+
uniform vec3 uBgColor;
|
|
18
|
+
uniform float uSpeed;
|
|
19
|
+
|
|
20
|
+
varying vec2 vUv;
|
|
21
|
+
|
|
22
|
+
void main() {
|
|
23
|
+
// Normalize UV to -1 to 1
|
|
24
|
+
vec2 uv = vUv * 2.0 - 1.0;
|
|
25
|
+
|
|
26
|
+
// Hoziron offset
|
|
27
|
+
float horizon = 0.0;
|
|
28
|
+
float fov = 0.5;
|
|
29
|
+
|
|
30
|
+
// 3D Projection Logic
|
|
31
|
+
// We only care about the bottom half for the floor
|
|
32
|
+
if (uv.y > horizon) {
|
|
33
|
+
// Sky (simple gradient or solid)
|
|
34
|
+
gl_FragColor = vec4(uBgColor, 1.0);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Project the 2D pixel to 3D floor coordinates
|
|
39
|
+
// x = uv.x / |y| to flare out perspective
|
|
40
|
+
// z = 1.0 / |y| to simulate depth
|
|
41
|
+
float floorY = abs(uv.y - horizon);
|
|
42
|
+
vec3 coord = vec3(uv.x / floorY, floorY, 1.0 / floorY);
|
|
43
|
+
|
|
44
|
+
// Move the grid by time
|
|
45
|
+
coord.z += uTime * uSpeed;
|
|
46
|
+
|
|
47
|
+
// Grid Logic
|
|
48
|
+
vec2 gridUV = coord.xz * fov;
|
|
49
|
+
vec2 grid = fract(gridUV) - 0.5;
|
|
50
|
+
|
|
51
|
+
// Thickness of lines (derivative for anti-aliasing approximation or hard coded)
|
|
52
|
+
float line = min(abs(grid.x), abs(grid.y));
|
|
53
|
+
|
|
54
|
+
float gridVal = 1.0 - smoothstep(0.0, 0.05 * coord.z, line);
|
|
55
|
+
|
|
56
|
+
// Fade out grid near horizon (fog)
|
|
57
|
+
float fog = smoothstep(0.0, 1.5, floorY);
|
|
58
|
+
|
|
59
|
+
vec3 color = mix(uBgColor, uGridColor, gridVal * fog);
|
|
60
|
+
|
|
61
|
+
gl_FragColor = vec4(color, 1.0);
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
uniforms: any;
|
|
66
|
+
|
|
67
|
+
constructor(config: RetroGridConfig) {
|
|
68
|
+
const grid = new Color(config.gridColor);
|
|
69
|
+
const bg = new Color(config.backgroundColor);
|
|
70
|
+
|
|
71
|
+
this.uniforms = {
|
|
72
|
+
uGridColor: { value: [grid.r, grid.g, grid.b] },
|
|
73
|
+
uBgColor: { value: [bg.r, bg.g, bg.b] },
|
|
74
|
+
uSpeed: { value: config.speed ?? 1.0 },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Color } from 'ogl';
|
|
2
|
+
import { ShaderPlugin } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export type SoftStarfieldConfig = {
|
|
5
|
+
/** Background gradient bottom/top */
|
|
6
|
+
backgroundBottom: string;
|
|
7
|
+
backgroundTop: string;
|
|
8
|
+
/** Star color (usually near-white) */
|
|
9
|
+
starColor?: string; // default "#ffffff"
|
|
10
|
+
/** Star density multiplier */
|
|
11
|
+
density?: number; // default 1.0
|
|
12
|
+
/** Star size multiplier */
|
|
13
|
+
size?: number; // default 1.0
|
|
14
|
+
/** Twinkle amount */
|
|
15
|
+
twinkle?: number; // default 0.35
|
|
16
|
+
/** Nebula tint */
|
|
17
|
+
nebulaColor?: string; // default "#6a5cff"
|
|
18
|
+
/** Nebula strength */
|
|
19
|
+
nebula?: number; // default 0.35
|
|
20
|
+
/** Motion speed */
|
|
21
|
+
speed?: number; // default 0.2
|
|
22
|
+
/** Grain 0.. */
|
|
23
|
+
grainAmount?: number; // default 0.04
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class SoftStarfieldPlugin implements ShaderPlugin {
|
|
27
|
+
name = 'soft-starfield';
|
|
28
|
+
|
|
29
|
+
fragmentShader = /* glsl */ `
|
|
30
|
+
precision highp float;
|
|
31
|
+
uniform float uTimeInternal;
|
|
32
|
+
uniform vec2 uResolution;
|
|
33
|
+
uniform vec3 uBg0;
|
|
34
|
+
uniform vec3 uBg1;
|
|
35
|
+
uniform vec3 uStar;
|
|
36
|
+
uniform vec3 uNebula;
|
|
37
|
+
uniform float uDensity;
|
|
38
|
+
uniform float uSize;
|
|
39
|
+
uniform float uTwinkle;
|
|
40
|
+
uniform float uNebulaAmt;
|
|
41
|
+
uniform float uGrain;
|
|
42
|
+
|
|
43
|
+
varying vec2 vUv;
|
|
44
|
+
|
|
45
|
+
float hash12(vec2 p) {
|
|
46
|
+
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
47
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
48
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
float noise(vec2 p) {
|
|
52
|
+
vec2 i = floor(p);
|
|
53
|
+
vec2 f = fract(p);
|
|
54
|
+
float a = hash12(i);
|
|
55
|
+
float b = hash12(i + vec2(1.0, 0.0));
|
|
56
|
+
float c = hash12(i + vec2(0.0, 1.0));
|
|
57
|
+
float d = hash12(i + vec2(1.0, 1.0));
|
|
58
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
59
|
+
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
float fbm(vec2 p) {
|
|
63
|
+
float v = 0.0;
|
|
64
|
+
float a = 0.5;
|
|
65
|
+
mat2 rot = mat2(0.80, -0.60, 0.60, 0.80);
|
|
66
|
+
for (int i = 0; i < 5; i++) {
|
|
67
|
+
v += a * noise(p);
|
|
68
|
+
p = rot * p * 2.02 + 19.19;
|
|
69
|
+
a *= 0.55;
|
|
70
|
+
}
|
|
71
|
+
return v;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Simple cell-star: one star candidate per cell, softly drawn.
|
|
75
|
+
float starLayer(vec2 uv, float scale, float t) {
|
|
76
|
+
vec2 p = uv * scale;
|
|
77
|
+
vec2 i = floor(p);
|
|
78
|
+
vec2 f = fract(p) - 0.5;
|
|
79
|
+
float rnd = hash12(i);
|
|
80
|
+
|
|
81
|
+
// place star within cell
|
|
82
|
+
vec2 o = vec2(hash12(i + 13.1), hash12(i + 71.7)) - 0.5;
|
|
83
|
+
vec2 d = f - o * 0.45;
|
|
84
|
+
|
|
85
|
+
// size distribution: many small, few larger
|
|
86
|
+
float sz = mix(0.012, 0.035, pow(rnd, 7.0)) * uSize;
|
|
87
|
+
float core = exp(-dot(d, d) / (sz * sz));
|
|
88
|
+
|
|
89
|
+
// twinkle: slow, subtle (avoid distracting flicker)
|
|
90
|
+
float tw = 1.0 + (sin((rnd * 12.0) + t * 1.5) * 0.5 + 0.5) * uTwinkle;
|
|
91
|
+
|
|
92
|
+
// probability via rnd threshold (density)
|
|
93
|
+
float present = step(0.55, rnd) * clamp(uDensity, 0.0, 3.0);
|
|
94
|
+
return core * tw * present;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
void main() {
|
|
98
|
+
float aspect = uResolution.x / uResolution.y;
|
|
99
|
+
vec2 uv = vUv;
|
|
100
|
+
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
|
|
101
|
+
float t = uTimeInternal;
|
|
102
|
+
|
|
103
|
+
// Background gradient
|
|
104
|
+
vec3 col = mix(uBg0, uBg1, smoothstep(-0.6, 0.8, p.y));
|
|
105
|
+
|
|
106
|
+
// Nebula: soft, low-contrast clouds
|
|
107
|
+
float n = fbm(p * 1.35 + vec2(-0.05 * t, 0.02 * t));
|
|
108
|
+
float n2 = fbm(p * 2.10 + vec2(0.03 * t, -0.04 * t));
|
|
109
|
+
float neb = smoothstep(0.25, 0.85, n * 0.75 + n2 * 0.35);
|
|
110
|
+
col += uNebula * neb * uNebulaAmt;
|
|
111
|
+
|
|
112
|
+
// Stars: 3 layers parallax-ish
|
|
113
|
+
float s1 = starLayer(uv + vec2(-0.010 * t, 0.006 * t), 55.0, t);
|
|
114
|
+
float s2 = starLayer(uv + vec2(-0.020 * t, 0.010 * t), 90.0, t + 7.7) * 0.8;
|
|
115
|
+
float s3 = starLayer(uv + vec2(-0.035 * t, 0.016 * t), 140.0, t + 13.3) * 0.6;
|
|
116
|
+
float stars = clamp(s1 + s2 + s3, 0.0, 1.75);
|
|
117
|
+
col += uStar * stars;
|
|
118
|
+
|
|
119
|
+
// Grain
|
|
120
|
+
float g = (hash12(uv * uResolution + t * 60.0) - 0.5) * 2.0;
|
|
121
|
+
col += g * uGrain;
|
|
122
|
+
|
|
123
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
uniforms: any;
|
|
128
|
+
private speed: number;
|
|
129
|
+
|
|
130
|
+
constructor(config: SoftStarfieldConfig) {
|
|
131
|
+
const bg0 = new Color(config.backgroundBottom);
|
|
132
|
+
const bg1 = new Color(config.backgroundTop);
|
|
133
|
+
const star = new Color(config.starColor ?? '#ffffff');
|
|
134
|
+
const neb = new Color(config.nebulaColor ?? '#6a5cff');
|
|
135
|
+
this.speed = config.speed ?? 0.2;
|
|
136
|
+
|
|
137
|
+
this.uniforms = {
|
|
138
|
+
uBg0: { value: [bg0.r, bg0.g, bg0.b] },
|
|
139
|
+
uBg1: { value: [bg1.r, bg1.g, bg1.b] },
|
|
140
|
+
uStar: { value: [star.r, star.g, star.b] },
|
|
141
|
+
uNebula: { value: [neb.r, neb.g, neb.b] },
|
|
142
|
+
uDensity: { value: config.density ?? 1.0 },
|
|
143
|
+
uSize: { value: config.size ?? 1.0 },
|
|
144
|
+
uTwinkle: { value: config.twinkle ?? 0.35 },
|
|
145
|
+
uNebulaAmt: { value: config.nebula ?? 0.35 },
|
|
146
|
+
uGrain: { value: config.grainAmount ?? 0.04 },
|
|
147
|
+
uTimeInternal: { value: 0 },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
onRender(dt: number) {
|
|
152
|
+
this.uniforms.uTimeInternal.value += dt * 0.001 * this.speed;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|