mirror of
https://github.com/storytold/spark.git
synced 2026-10-09 00:09:53 +00:00
ab83e53579
* Added particle flow transition to examples * Added particle flow transition to examples * Added splat-shader-effects to examples
236 lines
8.5 KiB
HTML
236 lines
8.5 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Spark • GLSL Shaders</title>
|
|
<style>
|
|
body { margin: 0; }
|
|
canvas { touch-action: none; }
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "/examples/js/vendor/three/build/three.module.js",
|
|
"lil-gui": "/examples/js/vendor/lil-gui/dist/lil-gui.esm.js",
|
|
"@sparkjsdev/spark": "/dist/spark.module.js"
|
|
}
|
|
}
|
|
</script>
|
|
<script type="module">
|
|
import * as THREE from "three";
|
|
import { SparkControls, SplatMesh, dyno } from "@sparkjsdev/spark";
|
|
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
|
import GUI from "lil-gui";
|
|
|
|
// Scene setup
|
|
const scene = new THREE.Scene();
|
|
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
const renderer = new THREE.WebGLRenderer();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// Cat model setup
|
|
const splatURL = await getAssetFileURL("cat.spz");
|
|
const cat = new SplatMesh({ url: splatURL });
|
|
cat.quaternion.set(1, 0, 0, 0);
|
|
cat.position.set(0, 0, -1.5);
|
|
cat.scale.set(.5, .5, .5);
|
|
scene.add(cat);
|
|
|
|
const animateT = dyno.dynoFloat(0);
|
|
|
|
// Effect parameters and GUI
|
|
const effectParams = {
|
|
effect: "Disintegrate",
|
|
intensity: 0.8
|
|
};
|
|
|
|
const gui = new GUI({ title: "Fractal Effects" });
|
|
gui.add(effectParams, "effect", ["Electronic", "Deep Meditation", "Waves", "Disintegrate", "Flare"])
|
|
.name("Effect Type")
|
|
.onChange(() => cat.updateGenerator());
|
|
gui.add(effectParams, "intensity", 0, 1, 0.01)
|
|
.name("Intensity")
|
|
.onChange(() => cat.updateGenerator());
|
|
|
|
// Shader modifier setup
|
|
cat.objectModifier = dyno.dynoBlock(
|
|
{ gsplat: dyno.Gsplat },
|
|
{ gsplat: dyno.Gsplat },
|
|
({ gsplat }) => {
|
|
const d = new dyno.Dyno({
|
|
inTypes: {
|
|
gsplat: dyno.Gsplat,
|
|
t: "float",
|
|
effectType: "int",
|
|
intensity: "float"
|
|
},
|
|
outTypes: { gsplat: dyno.Gsplat },
|
|
globals: () => [
|
|
dyno.unindent(`
|
|
vec3 hash(vec3 p) {
|
|
return fract(sin(p*123.456)*123.456);
|
|
}
|
|
|
|
mat2 rot(float a) {
|
|
float s = sin(a), c = cos(a);
|
|
return mat2(c, -s, s, c);
|
|
}
|
|
|
|
vec3 headMovement(vec3 pos, float t) {
|
|
pos.xy *= rot(smoothstep(-1., -2., pos.y) * .2 * sin(t*2.));
|
|
return pos;
|
|
}
|
|
|
|
vec3 breathAnimation(vec3 pos, float t) {
|
|
float b = sin(t*1.5);
|
|
pos.yz *= rot(smoothstep(-1., -3., pos.y) * .15 * -b);
|
|
pos.z += .3;
|
|
pos.y += 1.2;
|
|
pos *= 1. + exp(-3. * length(pos)) * b;
|
|
pos.z -= .3;
|
|
pos.y -= 1.2;
|
|
return pos;
|
|
}
|
|
|
|
vec4 fractal1(vec3 pos, float t, float intensity) {
|
|
float m = 100.;
|
|
vec3 p = pos * .1;
|
|
p.y += .5;
|
|
for (int i = 0; i < 8; i++) {
|
|
p = abs(p) / clamp(abs(p.x * p.y), 0.3, 3.) - 1.;
|
|
p.xy *= rot(radians(90.));
|
|
if (i > 1) m = min(m, length(p.xy) + step(.3, fract(p.z * .5 + t * .5 + float(i) * .2)));
|
|
}
|
|
m = step(m, 0.5) * 1.3 * intensity;
|
|
return vec4(-pos.y * .3, 0.5, 0.7, .3) * intensity + m;
|
|
}
|
|
|
|
vec4 fractal2(vec3 center, vec3 scales, vec4 rgba, float t, float intensity) {
|
|
vec3 pos = center;
|
|
float splatSize = length(scales);
|
|
float pattern = exp(-50. * splatSize);
|
|
vec3 p = pos * .65;
|
|
pos.y += 2.;
|
|
float c = 0.;
|
|
float l, l2 = length(p);
|
|
float m = 100.;
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
p.xyz = abs(p.xyz) / dot(p.xyz, p.xyz) - .8;
|
|
l = length(p.xyz);
|
|
c += exp(-1. * abs(l - l2) * (1. + sin(t * 1.5 + pos.y)));
|
|
l2 = length(p.xyz);
|
|
m = min(m, length(p.xyz));
|
|
}
|
|
|
|
c = smoothstep(0.3, 0.5, m + sin(t * 1.5 + pos.y * .5)) + c * .1;
|
|
return vec4(vec3(length(rgba.rgb)) * vec3(c, c*c, c*c*c) * intensity,
|
|
rgba.a * exp(-20. * splatSize) * m * intensity);
|
|
}
|
|
|
|
vec4 sin3D(vec3 p, float t) {
|
|
float m = exp(-2. * length(sin(p * 5. + t * 3.))) * 5.;
|
|
return vec4(m) + .3;
|
|
}
|
|
|
|
vec4 disintegrate(vec3 pos, float t, float intensity) {
|
|
vec3 p = pos + (hash(pos) * 2. - 1.) * intensity;
|
|
float tt = smoothstep(-1., 0.5, -sin(t + -pos.y * .5));
|
|
p.xz *= rot(tt * 2. + p.y * 2. * tt);
|
|
return vec4(mix(p, pos, tt), tt);
|
|
}
|
|
|
|
vec4 flare(vec3 pos, float t) {
|
|
vec3 p = vec3(0., -1.5, 0.);
|
|
float tt = smoothstep(-1., .5, sin(t + hash(pos).x));
|
|
tt = tt * tt;
|
|
p.x += sin(t * 2.) * tt;
|
|
p.z += sin(t * 2.) * tt;
|
|
p.y += sin(t) * tt;
|
|
return vec4(mix(pos, p, tt), tt);
|
|
}
|
|
`)
|
|
],
|
|
statements: ({ inputs, outputs }) => dyno.unindentLines(`
|
|
${outputs.gsplat} = ${inputs.gsplat};
|
|
|
|
vec3 localPos = ${inputs.gsplat}.center;
|
|
vec3 splatScales = ${inputs.gsplat}.scales;
|
|
vec4 splatColor = ${inputs.gsplat}.rgba;
|
|
|
|
if (${inputs.effectType} == 1) {
|
|
${outputs.gsplat}.center = headMovement(localPos, ${inputs.t});
|
|
vec4 effect1 = fractal1(localPos, ${inputs.t}, ${inputs.intensity});
|
|
${outputs.gsplat}.rgba.rgba = mix(splatColor, splatColor*effect1, ${inputs.intensity});
|
|
}
|
|
else if (${inputs.effectType} == 2) {
|
|
vec4 effectColor = fractal2(localPos, splatScales, splatColor, ${inputs.t}, ${inputs.intensity});
|
|
${outputs.gsplat}.rgba.rgba = mix(splatColor, effectColor, ${inputs.intensity});
|
|
${outputs.gsplat}.center = breathAnimation(localPos, ${inputs.t});
|
|
}
|
|
else if (${inputs.effectType} == 3) {
|
|
vec4 effect = sin3D(localPos, ${inputs.t});
|
|
${outputs.gsplat}.rgba.rgba = mix(splatColor, splatColor*effect, ${inputs.intensity});
|
|
vec3 pos = localPos;
|
|
pos.y += 1.;
|
|
pos *= (1. + effect.x * .05 * ${inputs.intensity});
|
|
pos.y -= 1.;
|
|
${outputs.gsplat}.center = pos;
|
|
}
|
|
else if (${inputs.effectType} == 5) {
|
|
vec4 e = disintegrate(localPos, ${inputs.t}, ${inputs.intensity});
|
|
${outputs.gsplat}.center = e.xyz;
|
|
${outputs.gsplat}.scales = mix(vec3(.01, .01, .01), ${inputs.gsplat}.scales, e.w);
|
|
}
|
|
else if (${inputs.effectType} == 4) {
|
|
vec4 e = flare(localPos, ${inputs.t});
|
|
${outputs.gsplat}.center = e.xyz;
|
|
${outputs.gsplat}.rgba.rgb = mix(splatColor.rgb, vec3(1.), abs(e.w));
|
|
${outputs.gsplat}.rgba.a = mix(splatColor.a, 0.3, abs(e.w));
|
|
}
|
|
`),
|
|
});
|
|
|
|
// Map effect names to type integers
|
|
const effectTypeMap = {
|
|
"Electronic": 1,
|
|
"Deep Meditation": 2,
|
|
"Waves": 3,
|
|
"Flare": 4,
|
|
"Disintegrate": 5
|
|
};
|
|
|
|
const effectType = effectTypeMap[effectParams.effect] || 1;
|
|
|
|
gsplat = d.apply({
|
|
gsplat,
|
|
t: animateT,
|
|
effectType: dyno.dynoInt(effectType),
|
|
intensity: dyno.dynoFloat(effectParams.intensity)
|
|
}).gsplat;
|
|
|
|
return { gsplat };
|
|
}
|
|
);
|
|
|
|
cat.updateGenerator();
|
|
|
|
// Animation loop
|
|
const controls = new SparkControls({ canvas: renderer.domElement });
|
|
renderer.setAnimationLoop(function animate(time) {
|
|
animateT.value = time / 1000;
|
|
cat.updateVersion();
|
|
cat.position.set(0, -.7, -2.5);
|
|
cat.rotation.set(Math.PI, time / 2000, 0);
|
|
controls.update(camera);
|
|
renderer.render(scene, camera);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|