Setting the file. One moment.
Sdf · Frost Sequence Camera Orbit · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page Three Mesh BVH LICENSE
312
export function sampleInterior
— line 312
This file
Number 10.66
Position 66 of 76
Type TypeScript
Size 11 KB
Lines 364 source/src/shape/ sdf.ts
TypeScript · 364 lines · 11 KB
,
clamp
,
sign
,
select
,
texture3D
}
=
6 tsl;
7 import type { ShapeName } from "../dials/defaults" ;
8
9 type N = any ;
10
11 export interface ShapeSpec {
12 name : ShapeName ;
13 size : number ;
14 tubeRatio : number ;
15 /** Half extent of the bounding cube used for the erosion field (object space). */
16 bound : number ;
17 /** CPU signed distance. */
18 sdf : ( x : number , y : number , z : number ) => number ;
19 /** TSL signed distance for a vec3 node. */
20 sdfNode : ( p : N ) => N ;
21 /** Approximate tube half-thickness (used for the surface-bias sampling shell). */
22 thickness : number ;
23 /** Object-space voxel spacing; follows the active shape during retargeting. */
24 voxelSize ?: N ;
25 }
26
27 const ICOSA_NORMALS : [ number , number , number ][] = (() => {
28 const phi = ( 1 + Math. sqrt ( 5 )) / 2 ;
29 const n : [ number , number , number ][] = [];
30 const push = ( x : number , y : number , z : number ) => {
31 const l = Math. hypot (x, y, z);
32 n. push ([x / l, y / l, z / l]);
33 };
34 // 20 face normals of a regular icosahedron = vertices of a dodecahedron
35 for ( const sx of [ - 1 , 1 ]) for ( const sy of [ - 1 , 1 ]) for ( const sz of [ - 1 , 1 ]) push (sx, sy, sz);
36 for ( const s1 of [ - 1 , 1 ])
37 for ( const s2 of [ - 1 , 1 ]) {
38 push ( 0 , s1 / phi, s2 * phi);
39 push (s1 / phi, s2 * phi, 0 );
40 push (s1 * phi, 0 , s2 / phi);
41 }
42 return n;
43 })();
44
45 const CUBE_POS : [ number , number , number ][] = [];
46 const RECT_POS : [ number , number , number ][] = [];
47
48 export function makeShape (
49 name : ShapeName ,
50 size : number ,
51 tubeRatio : number ,
52 logoSDF ?: LogoSDF ,
53 ) : ShapeSpec {
54 const R = size,
55 r = size * tubeRatio;
56 switch (name) {
57 case "torus" : {
58 return {
59 name,
60 size,
61 tubeRatio,
62 bound: ( R + r) * 1.12 ,
63 thickness: r,
64 sdf : ( x , y , z ) => {
65 const qx = Math. hypot (x, y) - R ;
66 return Math. hypot (qx, z) - r;
67 },
68 sdfNode : ( p ) => length ( vec2 ( length (p.xy). sub ( R ), p.z)). sub (r),
69 };
70 }
71 case "sphere" : {
72 const rad = size * 1.15 ;
73 return {
74 name,
75 size,
76 tubeRatio,
77 bound: rad * 1.15 ,
78 thickness: rad,
79 sdf : ( x , y , z ) => Math. hypot (x, y, z) - rad,
80 sdfNode : ( p ) => length (p). sub (rad),
81 };
82 }
83 case "roundedBox" : {
84 const h = size * 0.92 ,
85 rr = size * 0.22 ;
86 return {
87 name,
88 size,
89 tubeRatio,
90 bound: (h + rr) * 1.12 ,
91 thickness: h,
92 sdf : ( x , y , z ) => {
93 const qx = Math. abs (x) - h + rr,
94 qy = Math. abs (y) - h + rr,
95 qz = Math. abs (z) - h * 0.55 + rr;
96 const ox = Math. max (qx, 0 ),
97 oy = Math. max (qy, 0 ),
98 oz = Math. max (qz, 0 );
99 return Math. hypot (ox, oy, oz) + Math. min (Math. max (qx, qy, qz), 0 ) - rr;
100 },
101 sdfNode : ( p ) => {
102 const q = abs (p). sub ( vec3 (h - rr, h - rr, h * 0.55 - rr));
103 return length ( max (q, 0 ))
104 . add ( min ( max (q.x, max (q.y, q.z)), 0 ))
105 . sub (rr);
106 },
107 };
108 }
109 case "pyramid" : {
110 // square pyramid, apex up (+y), base half-width b, height hgt, with a rounding radius
111 const b = size * 1.05 ,
112 hgt = size * 1.5 ,
113 rr = size * 0.06 ;
114 const ny = b / Math. hypot (b, hgt),
115 nx = hgt / Math. hypot (b, hgt);
116 const yOff = - hgt * 0.4 ;
117 return {
118 name,
119 size,
120 tubeRatio,
121 bound: Math. max (b, hgt) * 1.25 ,
122 thickness: b * 0.6 ,
123 sdf : ( x , y , z ) => {
124 y -= yOff;
125 const ax = Math. abs (x),
126 az = Math. abs (z);
127 const dSide = Math. max (ax * nx + y * ny - hgt * ny, az * nx + y * ny - hgt * ny);
128 const dBase = - y;
129 return Math. max (dSide, dBase) - rr;
130 },
131 sdfNode : ( p ) => {
132 const pp = vec3 (p.x, p.y. sub (yOff), p.z);
133 const ax = abs (pp.x),
134 az = abs (pp.z);
135 const dSide = max (
136 ax
137 . mul (nx)
138 . add (pp.y. mul (ny))
139 . sub (hgt * ny),
140 az
141 . mul (nx)
142 . add (pp.y. mul (ny))
143 . sub (hgt * ny),
144 );
145 return max (dSide, pp.y. negate ()). sub (rr);
146 },
147 };
148 }
149 case "icosahedron" : {
150 const h = size * 1.05 ,
151 rr = size * 0.05 ;
152 return {
153 name,
154 size,
155 tubeRatio,
156 bound: h * 1.3 ,
157 thickness: h * 0.8 ,
158 sdf : ( x , y , z ) => {
159 let d = - Infinity ;
160 for ( const n of ICOSA_NORMALS ) d = Math. max (d, x * n[ 0 ] + y * n[ 1 ] + z * n[ 2 ]);
161 return d - h - rr;
162 },
163 sdfNode : ( p ) => {
164 let d : N = float ( - 100 );
165 for ( const n of ICOSA_NORMALS ) d = max (d, dot (p, vec3 (n[ 0 ], n[ 1 ], n[ 2 ])));
166 return d. sub (h + rr);
167 },
168 };
169 }
170 case "logo" : {
171 // `size` scales the loaded mark uniformly (geometry, bound and distance field alike)
172 const ls = logoSDF ! ,
173 k = size;
174 const sampleBoundNode = (ls.sampleBoundNode ??= tsl. uniform (ls.sampleDomain.value));
175 return {
176 name,
177 size,
178 tubeRatio,
179 bound: ls.bound * k,
180 thickness: ls.thickness * k,
181 voxelSize: sampleBoundNode. mul (( 2 * k) / ls.res),
182 sdf : ( x , y , z ) => ls. sample (x / k, y / k, z / k) * k,
183 sdfNode : ( p ) => {
184 const sampleBound = sampleBoundNode. mul (k);
185 const uvw = p. div (sampleBound. mul ( 2 )). add ( 0.5 );
186 const distance = texture3D (ls.texture, uvw)
187 .r. mul (ls.range * 2 )
188 . sub (ls.range)
189 . mul (k);
190 const outside = max ( abs (p). sub (sampleBound), 0 );
191 return select (
192 max (outside.x, max (outside.y, outside.z)). greaterThan ( 0 ),
193 max (distance, length (outside)),
194 distance,
195 );
196 },
197 };
198 }
199 }
200 }
201
202 /** Voxelised SDF for arbitrary meshes (logo). Values stored normalised: (d + range) / (2 range). */
203 export interface LogoSDF {
204 texture : THREE . Data3DTexture ;
205 res : number ;
206 bound : number ;
207 range : number ;
208 thickness : number ;
209 data : Float32Array ;
210 sample : ( x : number , y : number , z : number ) => number ;
211 sampleDomain : { value : number };
212 sampleBoundNode ?: N ;
213 }
214
215 /** Central-difference normal from the CPU sdf. */
216 export function sdfNormal (
217 s : ShapeSpec ,
218 x : number ,
219 y : number ,
220 z : number ,
221 e = 1e-3 ,
222 ) : [ number , number , number ] {
223 const nx = s. sdf (x + e, y, z) - s. sdf (x - e, y, z);
224 const ny = s. sdf (x, y + e, z) - s. sdf (x, y - e, z);
225 const nz = s. sdf (x, y, z + e) - s. sdf (x, y, z - e);
226 const l = Math. hypot (nx, ny, nz) || 1 ;
227 return [nx / l, ny / l, nz / l];
228 }
229
230 /** TSL gradient normal from the shape sdf. */
231 export const sdfNormalNode = ( s : ShapeSpec , p : N , e = 0.002 ) => {
232 const ex = vec3 (e, 0 , 0 ),
233 ey = vec3 ( 0 , e, 0 ),
234 ez = vec3 ( 0 , 0 , e);
235 const n = vec3 (
236 s. sdfNode (p. add (ex)). sub (s. sdfNode (p. sub (ex))),
237 s. sdfNode (p. add (ey)). sub (s. sdfNode (p. sub (ey))),
238 s. sdfNode (p. add (ez)). sub (s. sdfNode (p. sub (ez))),
239 );
240 return n. div ( max ( length (n), 1e-6 ));
241 };
242
243 export interface RayHit {
244 entry : THREE . Vector3 ;
245 exit : THREE . Vector3 ;
246 tEntry : number ;
247 tExit : number ;
248 }
249
250 /**
251 * Sphere-trace the object-space sdf. Returns entry and exit points along the ray, or null.
252 * `origin`/`dir` in object space; dir normalised.
253 */
254 export function raycastSDF (
255 s : ShapeSpec ,
256 origin : THREE . Vector3 ,
257 dir : THREE . Vector3 ,
258 maxDist = 60 ,
259 target ?: RayHit ,
260 ) : RayHit | null {
261 let t = 0 ;
262 let px = origin.x,
263 py = origin.y,
264 pz = origin.z;
265 // enter
266 let entered = false ;
267 for ( let i = 0 ; i < 160 ; i ++ ) {
268 px = dir.x * t + origin.x;
269 py = dir.y * t + origin.y;
270 pz = dir.z * t + origin.z;
271 const d = s. sdf (px, py, pz);
272 if (d < 0.0015 ) {
273 entered = true ;
274 break ;
275 }
276 t += Math. max (d, 0.002 );
277 if (t > maxDist) break ;
278 }
279 if ( ! entered) return null ;
280 const tEntry = t;
281 const hit = target ?? {
282 entry: new THREE . Vector3 (),
283 exit: new THREE . Vector3 (),
284 tEntry: 0 ,
285 tExit: 0 ,
286 };
287 hit.entry. set (px, py, pz);
288 // exit: march inside using the interior distance
289 let tt = t + 0.004 ;
290 for ( let i = 0 ; i < 200 ; i ++ ) {
291 px = dir.x * tt + origin.x;
292 py = dir.y * tt + origin.y;
293 pz = dir.z * tt + origin.z;
294 const d = s. sdf (px, py, pz);
295 if (d > 0.0015 ) break ;
296 tt += Math. max ( - d, 0.004 );
297 if (tt - t > s.bound * 4 ) break ;
298 }
299 hit.exit. set (dir.x * tt + origin.x, dir.y * tt + origin.y, dir.z * tt + origin.z);
300 hit.tEntry = tEntry;
301 hit.tExit = tt;
302 return hit;
303 }
304
305 export interface InteriorSamples {
306 positions : Float32Array ; // xyz per point
307 depths : Float32Array ; // 0 at the surface .. 1 deep inside (relative to thickness)
308 count : number ;
309 }
310
311 /** Rejection-sample the shape interior; `surfaceBias` of the points land in the outer `shell` fraction of thickness. */
312 export function sampleInterior (
313 s : ShapeSpec ,
314 count : number ,
315 surfaceBias : number ,
316 shell : number ,
317 rand : () => number ,
318 box ?: [ number , number , number ],
319 ) : InteriorSamples {
320 const positions = new Float32Array (count * 3 );
321 const depths = new Float32Array (count);
322 // FROST: optional half extents of the shape itself (a thin headline fills ~1% of the bound cube, so sampling
323 // the cube starves the guard below and leaves most points at the origin)
324 const BX = box ? box[ 0 ] : s.bound,
325 BY = box ? box[ 1 ] : s.bound,
326 BZ = box ? box[ 2 ] : s.bound;
327 const B = s.bound;
328 const shellDepth = s.thickness * shell;
329 const nSurf = Math. round (count * surfaceBias);
330 let i = 0 ;
331 let guard = 0 ;
332 while (i < count && guard < count * 400 ) {
333 guard ++ ;
334 const x = ( rand () * 2 - 1 ) * BX ,
335 y = ( rand () * 2 - 1 ) * BY ,
336 z = ( rand () * 2 - 1 ) * BZ ;
337 const d = s. sdf (x, y, z);
338 if (d >= 0 ) continue ;
339 const depth = - d;
340 const wantSurface = i < nSurf;
341 // Thin glyphs may have no deep core. Relax the bias after the bounded first
342 // pass; never silently return zero-filled homes at the origin.
343 if (guard < count * 60 ) {
344 if (wantSurface && depth > shellDepth) continue ;
345 if ( ! wantSurface && depth <= shellDepth) continue ;
346 }
347 positions[i * 3 ] = x;
348 positions[i * 3 + 1 ] = y;
349 positions[i * 3 + 2 ] = z;
350 depths[i] = Math. min ( 1 , depth / s.thickness);
351 i ++ ;
352 }
353 if (i !== count)
354 throw new Error ( `Frost: sampled only ${ i }/${ count } interior homes; check shape bounds and SDF` );
355 return { positions, depths, count: i };
356 }
357
358 void CUBE_POS ;
359 void RECT_POS ;
360 void sqrt;
361 void clamp;
362 void sign;
363 void select;
364 void Fn;