Skip to content

Commit 7067472

Browse files
committed
cleanup hardcoded constants, add multi-object transitions
1 parent 95d32bd commit 7067472

File tree

1 file changed

+159
-73
lines changed

1 file changed

+159
-73
lines changed

examples/splat-transitions/index.html

Lines changed: 159 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
}
2525
</script>
2626
<script type="module">
27-
28-
2927
import {
3028
dyno,
3129
SparkControls,
@@ -34,72 +32,113 @@
3432
} from "@sparkjsdev/spark";
3533
import * as THREE from "three";
3634
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
35+
import { GUI } from "lil-gui";
3736

38-
const TEST_ASSET_A = await getAssetFileURL("branzino-amarin.spz");
39-
const TEST_ASSET_B = await getAssetFileURL("pad-thai.spz");
37+
const TEST_ASSETS = [
38+
"branzino-amarin.spz",
39+
"pad-thai.spz",
40+
"robot-head.spz",
41+
"cat.spz",
42+
];
43+
44+
const PARAMETERS = {
45+
splat_coverage: 1.0,
46+
sphere_radius: 0.5,
47+
speed_multipler: 1.0,
48+
rotation: true,
49+
pause: false,
50+
};
51+
52+
function getTransitionState(t, fade_in_time, fade_out_time, period) {
53+
// inputs:
54+
// unnormalized time t
55+
// fade in and fade out (assumed to take 1.0 unnormalized time units)
56+
// period (assumed to be an integer number of unnormalized time units)
57+
// returns:
58+
// dynobool for whether transition is active
59+
// dynobool for whether transition is fading in or out
60+
// dynofloat for the normalized time of the transition
61+
const dynoOne = dyno.dynoFloat(1.0);
62+
const wrap_t = dyno.mod(t, period);
63+
const norm_t = dyno.mod(t, dynoOne);
64+
const is_fade_in = dyno.and(
65+
dyno.greaterThan(wrap_t, fade_in_time),
66+
dyno.lessThan(wrap_t, dyno.add(fade_in_time, dynoOne)),
67+
);
68+
const is_fade_out = dyno.and(
69+
dyno.greaterThan(wrap_t, fade_out_time),
70+
dyno.lessThan(wrap_t, dyno.add(fade_out_time, dynoOne)),
71+
);
72+
const in_transition = dyno.or(is_fade_in, is_fade_out);
73+
return { in_transition, is_fade_in, norm_t };
74+
}
4075

4176
function contractionDyno() {
42-
// t is assumed to be in [0, 1], any retiming should be done before this function.
77+
// this is a looping shader that periodically contracts and expands the splat.
78+
// t is assumed to be in normalized time [0, 1], any retiming should be done before this function.
4379
// fade_in is a boolean that indicates whether the splat is fading in or out.
4480
return new dyno.Dyno({
45-
inTypes: { gsplat: dyno.Gsplat, t: "float", fade_in: "bool" },
81+
inTypes: {
82+
gsplat: dyno.Gsplat,
83+
in_transition: "bool",
84+
fade_in: "bool",
85+
t: "float",
86+
splat_scale: "float",
87+
sphere_radius: "float"
88+
},
4689
outTypes: { gsplat: dyno.Gsplat },
4790
globals: () => [
4891
dyno.unindent(`
49-
float radius = 0.5;
5092
float churn = 0.05;
51-
float targetScale = 0.01;
52-
vec3 targetCenterOffset = vec3(0.0, 0.5, 0.0);
53-
vec3 eps = vec3(0.0, 0.0, 0.001);
54-
55-
56-
vec3 applyCenter(vec3 center, float t) {
57-
vec3 dir = normalize(center + eps - targetCenterOffset);
58-
vec3 targetCenter = dir * radius + targetCenterOffset;
59-
if (t < 0.25) {
93+
vec3 target_center = vec3(0.0, 0.5, 0.0);
94+
95+
vec3 applyCenter(vec3 center, float t, float sphere_radius) {
96+
vec3 dir = normalize(center - target_center);
97+
vec3 target_point = target_center + dir * sphere_radius;
98+
if (t < 0.25 || t > 0.75) {
6099
return center;
61100
} else if (t < 0.45) {
62-
return mix(center, targetCenter, pow((t - 0.25) * 5.0, 4.0));
101+
return mix(center, target_point, pow((t - 0.25) * 5.0, 4.0));
63102
} else if (t < 0.55) {
64-
float angle = (t - 0.45) * 10.0 * 2.0 * 3.14159265358979323846;
103+
float transition_t = (t - 0.45) * 10.0;
104+
float angle = transition_t * 2.0 * PI;
65105
vec3 rotvec = vec3(sin(angle), 0.0, cos(angle));
66-
return targetCenter+ cross(-dir, rotvec) * churn;
67-
} else if (t < 0.75) {
68-
return mix(targetCenter, center, pow((t - 0.55) * 5.0, 4.0));
106+
float strength = sin(transition_t * PI);
107+
return target_point + cross(dir, rotvec) * churn * strength;
69108
} else {
70-
return center;
109+
return mix(target_point, center, pow((t - 0.55) * 5.0, 4.0));
71110
}
72111
}
73-
74-
vec3 applyScale(vec3 scales, float t) {
75-
vec3 targetScales = targetScale * vec3(1.0, 1.0, 1.0);
112+
113+
vec3 applyScale(vec3 scales, float t, float target_scale) {
114+
vec3 target_scales = target_scale * vec3(1.0, 1.0, 1.0);
76115
if (t < 0.25) {
77116
return scales;
78117
} else if (t < 0.45) {
79-
return mix(scales, targetScales, pow((t - 0.25) * 5.0, 4.0));
118+
return mix(scales, target_scales, pow((t - 0.25) * 5.0, 2.0));
80119
} else if (t < 0.55) {
81-
return targetScales;
120+
return target_scales;
82121
} else if (t < 0.75) {
83-
return mix(targetScales, scales, pow((t - 0.55) * 5.0, 4.0));
122+
return mix(target_scales, scales, pow((t - 0.55) * 5.0, 2.0));
84123
} else {
85124
return scales;
86125
}
87126
}
88127
89128
float applyOpacity(float opacity, float t, bool fade_in) {
90129
if (fade_in) {
91-
if (t < 0.48) {
130+
if (t < 0.45) {
92131
return 0.0;
93-
} else if (t < 0.52) {
94-
return mix(0.0, opacity, (t - 0.48) * 25.0);
132+
} else if (t < 0.55) {
133+
return mix(0.0, opacity, (t - 0.45) * 10.0);
95134
} else {
96135
return opacity;
97136
}
98137
} else {
99-
if (t < 0.4) {
138+
if (t < 0.45) {
100139
return opacity;
101-
} else if (t < 0.6) {
102-
return mix(opacity, 0.0, (t - 0.4) * 5.0);
140+
} else if (t < 0.55) {
141+
return mix(opacity, 0.0, (t - 0.45) * 10.0);
103142
} else {
104143
return 0.0;
105144
}
@@ -109,43 +148,62 @@
109148
],
110149
statements: ({ inputs, outputs }) => dyno.unindentLines(`
111150
${outputs.gsplat} = ${inputs.gsplat};
112-
${outputs.gsplat}.center = applyCenter(${inputs.gsplat}.center, ${inputs.t});
113-
${outputs.gsplat}.scales = applyScale(${inputs.gsplat}.scales, ${inputs.t});
114-
${outputs.gsplat}.rgba.a = applyOpacity(${inputs.gsplat}.rgba.a, ${inputs.t}, ${inputs.fade_in});
151+
${outputs.gsplat}.center = applyCenter(${inputs.gsplat}.center, ${inputs.t}, ${inputs.sphere_radius});
152+
${outputs.gsplat}.scales = applyScale(${inputs.gsplat}.scales, ${inputs.t}, ${inputs.splat_scale});
153+
if (${inputs.in_transition}) {
154+
${outputs.gsplat}.rgba.a = applyOpacity(${inputs.gsplat}.rgba.a, ${inputs.t}, ${inputs.fade_in});
155+
} else {
156+
${outputs.gsplat}.rgba.a = 0.0;
157+
}
115158
`),
116159
});
117160
}
118161

119-
function getTransitionModifier(time, offset, period) {
162+
function getTransitionModifier(in_transition, fade_in, t, splat_scale, sphere_radius) {
120163
const contraction = contractionDyno();
121164
return dyno.dynoBlock(
122165
{ gsplat: dyno.Gsplat },
123166
{ gsplat: dyno.Gsplat },
124-
({ gsplat }) => {
125-
const normalized_time = dyno.add(dyno.div(time, period), offset);
126-
// 2x to handle wraparound
127-
const fade_in = dyno.lessThan(dyno.mod(normalized_time, dyno.dynoFloat(2.0)), dyno.dynoFloat(1.0));
128-
const retime = dyno.mod(normalized_time, dyno.dynoFloat(1.0));
129-
gsplat = contraction.apply({ gsplat, t: retime, fade_in }).gsplat;
167+
({ gsplat }) => {
168+
gsplat = contraction.apply({ gsplat,
169+
in_transition, fade_in, t, splat_scale, sphere_radius
170+
}).gsplat
130171
return { gsplat };
131172
},
132173
);
133174
}
134175

135-
function morphableSplatMesh(
136-
url,
176+
async function morphableSplatMesh(
177+
asset_name,
137178
time,
138-
offset,
179+
fade_in_time,
180+
fade_out_time,
139181
period,
182+
splat_coverage,
183+
sphere_radius,
140184
) {
185+
const { in_transition, is_fade_in, norm_t } =
186+
getTransitionState(time, fade_in_time, fade_out_time, period);
187+
const url = await getAssetFileURL(asset_name);
188+
141189
const splatMesh = new SplatMesh({
142190
url: url,
143-
worldModifier: getTransitionModifier(time, offset, period),
144191
onFrame: ({ mesh, time }) => {
145-
mesh.rotation.y = 0.05 * time;
146192
mesh.needsUpdate = true;
147193
}
148194
});
195+
await splatMesh.initialized; // wait to get splatCount
196+
const splat_scale = dyno.div(dyno.mul(splat_coverage, sphere_radius),
197+
dyno.dynoFloat(splatMesh.packedSplats.numSplats / 1000.0)
198+
);
199+
200+
splatMesh.worldModifier = getTransitionModifier(
201+
in_transition,
202+
is_fade_in,
203+
norm_t,
204+
splat_scale,
205+
sphere_radius
206+
);
149207
splatMesh.updateGenerator();
150208
return splatMesh;
151209
}
@@ -165,11 +223,12 @@
165223
50,
166224
window.innerWidth / window.innerHeight,
167225
0.01,
168-
2000,
226+
1000,
169227
);
170-
camera.position.set(0, 5, 5); // Move camera back to see the scene
228+
camera.position.set(0, 5, 5);
171229
camera.lookAt(0, 0, 0);
172230
scene.add(camera);
231+
173232
const sparkControls = new SparkControls({ canvas: renderer.domElement });
174233
function handleResize() {
175234
const width = window.innerWidth;
@@ -181,38 +240,65 @@
181240

182241
handleResize();
183242
window.addEventListener("resize", handleResize);
243+
184244
const time = dyno.dynoFloat(0.0);
185245

186-
async function loadInitialScene() {
246+
async function loadAssets(splat_coverage, sphere_radius) {
187247
console.log("Loading initial scene...");
188-
const period = dyno.dynoFloat(1.0);
189-
190-
const splat_mesh_a = morphableSplatMesh(
191-
TEST_ASSET_A,
192-
time,
193-
dyno.dynoFloat(0.0), //offset
194-
period,
195-
);
196-
const splat_mesh_b = morphableSplatMesh(
197-
TEST_ASSET_B,
198-
time,
199-
dyno.dynoFloat(1.0), //offset
200-
period,
201-
);
202-
splat_mesh_a.quaternion.set(1, 0, 0, 0);
203-
splat_mesh_b.quaternion.set(1, 0, 0, 0);
204-
scene.add(splat_mesh_a);
205-
scene.add(splat_mesh_b);
248+
const splat_meshes = [];
249+
const period = dyno.dynoFloat(TEST_ASSETS.length);
250+
for (let i=0; i<TEST_ASSETS.length; i++) {
251+
console.log(TEST_ASSETS[i], (i+1) % TEST_ASSETS.length);
252+
const splat_mesh = await morphableSplatMesh(
253+
TEST_ASSETS[i],
254+
time,
255+
dyno.dynoFloat(i), //fade_in_time
256+
dyno.dynoFloat((i+1) % TEST_ASSETS.length), //fade_out_time
257+
period,
258+
splat_coverage,
259+
sphere_radius,
260+
);
261+
splat_mesh.quaternion.set(1, 0, 0, 0);
262+
scene.add(splat_mesh);
263+
splat_meshes.push(splat_mesh);
264+
}
265+
return splat_meshes;
206266
}
207-
await loadInitialScene();
208-
267+
268+
const sphere_radius_dyno = dyno.dynoFloat(PARAMETERS.sphere_radius)
269+
const splat_coverage_dyno = dyno.dynoFloat(PARAMETERS.splat_coverage)
270+
const splat_meshes = await loadAssets(splat_coverage_dyno, sphere_radius_dyno);
271+
272+
const gui = new GUI();
273+
gui.add(PARAMETERS, "sphere_radius").min(0.1).max(8.0).step(0.01).onChange((value) => {
274+
sphere_radius_dyno.value = value;
275+
});
276+
gui.add(PARAMETERS, "splat_coverage").min(0.1).max(1.0).step(0.01).onChange((value) => {
277+
splat_coverage_dyno.value = value;
278+
});
279+
gui.add(PARAMETERS, "speed_multipler").min(0.1).max(4.0).step(0.01);
280+
gui.add(PARAMETERS, "rotation");
281+
gui.add(PARAMETERS, "pause");
282+
209283
console.log("Starting render loop");
210284

211285
// Animation loop
286+
let lastTime = 0;
212287
renderer.setAnimationLoop((raw_time) => {
288+
raw_time *= 0.0005;
289+
const deltaTime = raw_time - (lastTime ?? raw_time);
290+
lastTime = raw_time;
213291
sparkControls.update(camera);
214292
renderer.render(scene, camera);
215-
time.value = raw_time * 0.00025;
293+
294+
if (!PARAMETERS.pause) {
295+
time.value +=deltaTime * PARAMETERS.speed_multipler;
296+
if (PARAMETERS.rotation) {
297+
for (const splat_mesh of splat_meshes) {
298+
splat_mesh.rotation.y += deltaTime * PARAMETERS.speed_multipler;
299+
}
300+
}
301+
}
216302
});
217303
</script>
218304
</body>

0 commit comments

Comments
 (0)