|
24 | 24 | } |
25 | 25 | </script> |
26 | 26 | <script type="module"> |
27 | | - |
28 | | - |
29 | 27 | import { |
30 | 28 | dyno, |
31 | 29 | SparkControls, |
|
34 | 32 | } from "@sparkjsdev/spark"; |
35 | 33 | import * as THREE from "three"; |
36 | 34 | import { getAssetFileURL } from "/examples/js/get-asset-url.js"; |
| 35 | + import { GUI } from "lil-gui"; |
37 | 36 |
|
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 | + } |
40 | 75 |
|
41 | 76 | 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. |
43 | 79 | // fade_in is a boolean that indicates whether the splat is fading in or out. |
44 | 80 | 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 | + }, |
46 | 89 | outTypes: { gsplat: dyno.Gsplat }, |
47 | 90 | globals: () => [ |
48 | 91 | dyno.unindent(` |
49 | | - float radius = 0.5; |
50 | 92 | 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) { |
60 | 99 | return center; |
61 | 100 | } 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)); |
63 | 102 | } 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; |
65 | 105 | 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; |
69 | 108 | } else { |
70 | | - return center; |
| 109 | + return mix(target_point, center, pow((t - 0.55) * 5.0, 4.0)); |
71 | 110 | } |
72 | 111 | } |
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); |
76 | 115 | if (t < 0.25) { |
77 | 116 | return scales; |
78 | 117 | } 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)); |
80 | 119 | } else if (t < 0.55) { |
81 | | - return targetScales; |
| 120 | + return target_scales; |
82 | 121 | } 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)); |
84 | 123 | } else { |
85 | 124 | return scales; |
86 | 125 | } |
87 | 126 | } |
88 | 127 |
|
89 | 128 | float applyOpacity(float opacity, float t, bool fade_in) { |
90 | 129 | if (fade_in) { |
91 | | - if (t < 0.48) { |
| 130 | + if (t < 0.45) { |
92 | 131 | 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); |
95 | 134 | } else { |
96 | 135 | return opacity; |
97 | 136 | } |
98 | 137 | } else { |
99 | | - if (t < 0.4) { |
| 138 | + if (t < 0.45) { |
100 | 139 | 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); |
103 | 142 | } else { |
104 | 143 | return 0.0; |
105 | 144 | } |
|
109 | 148 | ], |
110 | 149 | statements: ({ inputs, outputs }) => dyno.unindentLines(` |
111 | 150 | ${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 | + } |
115 | 158 | `), |
116 | 159 | }); |
117 | 160 | } |
118 | 161 |
|
119 | | - function getTransitionModifier(time, offset, period) { |
| 162 | + function getTransitionModifier(in_transition, fade_in, t, splat_scale, sphere_radius) { |
120 | 163 | const contraction = contractionDyno(); |
121 | 164 | return dyno.dynoBlock( |
122 | 165 | { gsplat: dyno.Gsplat }, |
123 | 166 | { 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 |
130 | 171 | return { gsplat }; |
131 | 172 | }, |
132 | 173 | ); |
133 | 174 | } |
134 | 175 |
|
135 | | - function morphableSplatMesh( |
136 | | - url, |
| 176 | + async function morphableSplatMesh( |
| 177 | + asset_name, |
137 | 178 | time, |
138 | | - offset, |
| 179 | + fade_in_time, |
| 180 | + fade_out_time, |
139 | 181 | period, |
| 182 | + splat_coverage, |
| 183 | + sphere_radius, |
140 | 184 | ) { |
| 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 | + |
141 | 189 | const splatMesh = new SplatMesh({ |
142 | 190 | url: url, |
143 | | - worldModifier: getTransitionModifier(time, offset, period), |
144 | 191 | onFrame: ({ mesh, time }) => { |
145 | | - mesh.rotation.y = 0.05 * time; |
146 | 192 | mesh.needsUpdate = true; |
147 | 193 | } |
148 | 194 | }); |
| 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 | + ); |
149 | 207 | splatMesh.updateGenerator(); |
150 | 208 | return splatMesh; |
151 | 209 | } |
|
165 | 223 | 50, |
166 | 224 | window.innerWidth / window.innerHeight, |
167 | 225 | 0.01, |
168 | | - 2000, |
| 226 | + 1000, |
169 | 227 | ); |
170 | | - camera.position.set(0, 5, 5); // Move camera back to see the scene |
| 228 | + camera.position.set(0, 5, 5); |
171 | 229 | camera.lookAt(0, 0, 0); |
172 | 230 | scene.add(camera); |
| 231 | + |
173 | 232 | const sparkControls = new SparkControls({ canvas: renderer.domElement }); |
174 | 233 | function handleResize() { |
175 | 234 | const width = window.innerWidth; |
|
181 | 240 |
|
182 | 241 | handleResize(); |
183 | 242 | window.addEventListener("resize", handleResize); |
| 243 | + |
184 | 244 | const time = dyno.dynoFloat(0.0); |
185 | 245 |
|
186 | | - async function loadInitialScene() { |
| 246 | + async function loadAssets(splat_coverage, sphere_radius) { |
187 | 247 | 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; |
206 | 266 | } |
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 | + |
209 | 283 | console.log("Starting render loop"); |
210 | 284 |
|
211 | 285 | // Animation loop |
| 286 | + let lastTime = 0; |
212 | 287 | renderer.setAnimationLoop((raw_time) => { |
| 288 | + raw_time *= 0.0005; |
| 289 | + const deltaTime = raw_time - (lastTime ?? raw_time); |
| 290 | + lastTime = raw_time; |
213 | 291 | sparkControls.update(camera); |
214 | 292 | 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 | + } |
216 | 302 | }); |
217 | 303 | </script> |
218 | 304 | </body> |
|
0 commit comments