Skip to content

Commit ab83e53

Browse files
authored
Splat shader effects (#141)
* Added particle flow transition to examples * Added particle flow transition to examples * Added splat-shader-effects to examples
1 parent a6bdb5f commit ab83e53

File tree

1 file changed

+235
-0
lines changed

1 file changed

+235
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Spark • GLSL Shaders</title>
7+
<style>
8+
body { margin: 0; }
9+
canvas { touch-action: none; }
10+
</style>
11+
</head>
12+
13+
<body>
14+
<script type="importmap">
15+
{
16+
"imports": {
17+
"three": "/examples/js/vendor/three/build/three.module.js",
18+
"lil-gui": "/examples/js/vendor/lil-gui/dist/lil-gui.esm.js",
19+
"@sparkjsdev/spark": "/dist/spark.module.js"
20+
}
21+
}
22+
</script>
23+
<script type="module">
24+
import * as THREE from "three";
25+
import { SparkControls, SplatMesh, dyno } from "@sparkjsdev/spark";
26+
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
27+
import GUI from "lil-gui";
28+
29+
// Scene setup
30+
const scene = new THREE.Scene();
31+
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
32+
const renderer = new THREE.WebGLRenderer();
33+
renderer.setSize(window.innerWidth, window.innerHeight);
34+
document.body.appendChild(renderer.domElement);
35+
36+
// Cat model setup
37+
const splatURL = await getAssetFileURL("cat.spz");
38+
const cat = new SplatMesh({ url: splatURL });
39+
cat.quaternion.set(1, 0, 0, 0);
40+
cat.position.set(0, 0, -1.5);
41+
cat.scale.set(.5, .5, .5);
42+
scene.add(cat);
43+
44+
const animateT = dyno.dynoFloat(0);
45+
46+
// Effect parameters and GUI
47+
const effectParams = {
48+
effect: "Disintegrate",
49+
intensity: 0.8
50+
};
51+
52+
const gui = new GUI({ title: "Fractal Effects" });
53+
gui.add(effectParams, "effect", ["Electronic", "Deep Meditation", "Waves", "Disintegrate", "Flare"])
54+
.name("Effect Type")
55+
.onChange(() => cat.updateGenerator());
56+
gui.add(effectParams, "intensity", 0, 1, 0.01)
57+
.name("Intensity")
58+
.onChange(() => cat.updateGenerator());
59+
60+
// Shader modifier setup
61+
cat.objectModifier = dyno.dynoBlock(
62+
{ gsplat: dyno.Gsplat },
63+
{ gsplat: dyno.Gsplat },
64+
({ gsplat }) => {
65+
const d = new dyno.Dyno({
66+
inTypes: {
67+
gsplat: dyno.Gsplat,
68+
t: "float",
69+
effectType: "int",
70+
intensity: "float"
71+
},
72+
outTypes: { gsplat: dyno.Gsplat },
73+
globals: () => [
74+
dyno.unindent(`
75+
vec3 hash(vec3 p) {
76+
return fract(sin(p*123.456)*123.456);
77+
}
78+
79+
mat2 rot(float a) {
80+
float s = sin(a), c = cos(a);
81+
return mat2(c, -s, s, c);
82+
}
83+
84+
vec3 headMovement(vec3 pos, float t) {
85+
pos.xy *= rot(smoothstep(-1., -2., pos.y) * .2 * sin(t*2.));
86+
return pos;
87+
}
88+
89+
vec3 breathAnimation(vec3 pos, float t) {
90+
float b = sin(t*1.5);
91+
pos.yz *= rot(smoothstep(-1., -3., pos.y) * .15 * -b);
92+
pos.z += .3;
93+
pos.y += 1.2;
94+
pos *= 1. + exp(-3. * length(pos)) * b;
95+
pos.z -= .3;
96+
pos.y -= 1.2;
97+
return pos;
98+
}
99+
100+
vec4 fractal1(vec3 pos, float t, float intensity) {
101+
float m = 100.;
102+
vec3 p = pos * .1;
103+
p.y += .5;
104+
for (int i = 0; i < 8; i++) {
105+
p = abs(p) / clamp(abs(p.x * p.y), 0.3, 3.) - 1.;
106+
p.xy *= rot(radians(90.));
107+
if (i > 1) m = min(m, length(p.xy) + step(.3, fract(p.z * .5 + t * .5 + float(i) * .2)));
108+
}
109+
m = step(m, 0.5) * 1.3 * intensity;
110+
return vec4(-pos.y * .3, 0.5, 0.7, .3) * intensity + m;
111+
}
112+
113+
vec4 fractal2(vec3 center, vec3 scales, vec4 rgba, float t, float intensity) {
114+
vec3 pos = center;
115+
float splatSize = length(scales);
116+
float pattern = exp(-50. * splatSize);
117+
vec3 p = pos * .65;
118+
pos.y += 2.;
119+
float c = 0.;
120+
float l, l2 = length(p);
121+
float m = 100.;
122+
123+
for (int i = 0; i < 10; i++) {
124+
p.xyz = abs(p.xyz) / dot(p.xyz, p.xyz) - .8;
125+
l = length(p.xyz);
126+
c += exp(-1. * abs(l - l2) * (1. + sin(t * 1.5 + pos.y)));
127+
l2 = length(p.xyz);
128+
m = min(m, length(p.xyz));
129+
}
130+
131+
c = smoothstep(0.3, 0.5, m + sin(t * 1.5 + pos.y * .5)) + c * .1;
132+
return vec4(vec3(length(rgba.rgb)) * vec3(c, c*c, c*c*c) * intensity,
133+
rgba.a * exp(-20. * splatSize) * m * intensity);
134+
}
135+
136+
vec4 sin3D(vec3 p, float t) {
137+
float m = exp(-2. * length(sin(p * 5. + t * 3.))) * 5.;
138+
return vec4(m) + .3;
139+
}
140+
141+
vec4 disintegrate(vec3 pos, float t, float intensity) {
142+
vec3 p = pos + (hash(pos) * 2. - 1.) * intensity;
143+
float tt = smoothstep(-1., 0.5, -sin(t + -pos.y * .5));
144+
p.xz *= rot(tt * 2. + p.y * 2. * tt);
145+
return vec4(mix(p, pos, tt), tt);
146+
}
147+
148+
vec4 flare(vec3 pos, float t) {
149+
vec3 p = vec3(0., -1.5, 0.);
150+
float tt = smoothstep(-1., .5, sin(t + hash(pos).x));
151+
tt = tt * tt;
152+
p.x += sin(t * 2.) * tt;
153+
p.z += sin(t * 2.) * tt;
154+
p.y += sin(t) * tt;
155+
return vec4(mix(pos, p, tt), tt);
156+
}
157+
`)
158+
],
159+
statements: ({ inputs, outputs }) => dyno.unindentLines(`
160+
${outputs.gsplat} = ${inputs.gsplat};
161+
162+
vec3 localPos = ${inputs.gsplat}.center;
163+
vec3 splatScales = ${inputs.gsplat}.scales;
164+
vec4 splatColor = ${inputs.gsplat}.rgba;
165+
166+
if (${inputs.effectType} == 1) {
167+
${outputs.gsplat}.center = headMovement(localPos, ${inputs.t});
168+
vec4 effect1 = fractal1(localPos, ${inputs.t}, ${inputs.intensity});
169+
${outputs.gsplat}.rgba.rgba = mix(splatColor, splatColor*effect1, ${inputs.intensity});
170+
}
171+
else if (${inputs.effectType} == 2) {
172+
vec4 effectColor = fractal2(localPos, splatScales, splatColor, ${inputs.t}, ${inputs.intensity});
173+
${outputs.gsplat}.rgba.rgba = mix(splatColor, effectColor, ${inputs.intensity});
174+
${outputs.gsplat}.center = breathAnimation(localPos, ${inputs.t});
175+
}
176+
else if (${inputs.effectType} == 3) {
177+
vec4 effect = sin3D(localPos, ${inputs.t});
178+
${outputs.gsplat}.rgba.rgba = mix(splatColor, splatColor*effect, ${inputs.intensity});
179+
vec3 pos = localPos;
180+
pos.y += 1.;
181+
pos *= (1. + effect.x * .05 * ${inputs.intensity});
182+
pos.y -= 1.;
183+
${outputs.gsplat}.center = pos;
184+
}
185+
else if (${inputs.effectType} == 5) {
186+
vec4 e = disintegrate(localPos, ${inputs.t}, ${inputs.intensity});
187+
${outputs.gsplat}.center = e.xyz;
188+
${outputs.gsplat}.scales = mix(vec3(.01, .01, .01), ${inputs.gsplat}.scales, e.w);
189+
}
190+
else if (${inputs.effectType} == 4) {
191+
vec4 e = flare(localPos, ${inputs.t});
192+
${outputs.gsplat}.center = e.xyz;
193+
${outputs.gsplat}.rgba.rgb = mix(splatColor.rgb, vec3(1.), abs(e.w));
194+
${outputs.gsplat}.rgba.a = mix(splatColor.a, 0.3, abs(e.w));
195+
}
196+
`),
197+
});
198+
199+
// Map effect names to type integers
200+
const effectTypeMap = {
201+
"Electronic": 1,
202+
"Deep Meditation": 2,
203+
"Waves": 3,
204+
"Flare": 4,
205+
"Disintegrate": 5
206+
};
207+
208+
const effectType = effectTypeMap[effectParams.effect] || 1;
209+
210+
gsplat = d.apply({
211+
gsplat,
212+
t: animateT,
213+
effectType: dyno.dynoInt(effectType),
214+
intensity: dyno.dynoFloat(effectParams.intensity)
215+
}).gsplat;
216+
217+
return { gsplat };
218+
}
219+
);
220+
221+
cat.updateGenerator();
222+
223+
// Animation loop
224+
const controls = new SparkControls({ canvas: renderer.domElement });
225+
renderer.setAnimationLoop(function animate(time) {
226+
animateT.value = time / 1000;
227+
cat.updateVersion();
228+
cat.position.set(0, -.7, -2.5);
229+
cat.rotation.set(Math.PI, time / 2000, 0);
230+
controls.update(camera);
231+
renderer.render(scene, camera);
232+
});
233+
</script>
234+
</body>
235+
</html>

0 commit comments

Comments
 (0)