1+ import tgpu , { type TgpuBuffer , type TgpuRoot , type VertexFlag } from 'typegpu' ;
12import * as d from 'typegpu/data' ;
2- import { Vertex } from './dataTypes' ;
3+ import * as std from 'typegpu/std' ;
4+ import { ComputeVertex , Vertex } from './dataTypes' ;
35
46/**
57 * Creates an icosphere with the specified level of subdivision
@@ -144,11 +146,8 @@ function createVertex(
144146 position : d . v4f ,
145147 normal : d . v4f ,
146148) : ReturnType < typeof Vertex > {
147- const color = d . vec4f ( 1 , 1 , 1 , 1 ) ;
148-
149149 return Vertex ( {
150150 position,
151- color,
152151 normal,
153152 } ) ;
154153}
@@ -163,3 +162,323 @@ function normalizeSafely(v: d.v4f): d.v4f {
163162 }
164163 return d . vec4f ( v . x / length , v . y / length , v . z / length , 1 ) ;
165164}
165+
166+ //////// GPU TERRITORY ////////
167+
168+ function getVertexAmount ( subdivisions : number ) : number {
169+ return 60 * 4 ** subdivisions ;
170+ }
171+
172+ function createBaseIcosphere ( smooth : boolean ) : d . Infer < typeof Vertex > [ ] {
173+ const goldenRatio = ( 1 + Math . sqrt ( 5 ) ) / 2 ;
174+
175+ const initialVertices : d . v4f [ ] = [
176+ // Top group
177+ d . vec4f ( - 1 , goldenRatio , 0 , 1 ) ,
178+ d . vec4f ( 1 , goldenRatio , 0 , 1 ) ,
179+ d . vec4f ( - 1 , - goldenRatio , 0 , 1 ) ,
180+ d . vec4f ( 1 , - goldenRatio , 0 , 1 ) ,
181+
182+ // Middle group
183+ d . vec4f ( 0 , - 1 , goldenRatio , 1 ) ,
184+ d . vec4f ( 0 , 1 , goldenRatio , 1 ) ,
185+ d . vec4f ( 0 , - 1 , - goldenRatio , 1 ) ,
186+ d . vec4f ( 0 , 1 , - goldenRatio , 1 ) ,
187+
188+ // Bottom group
189+ d . vec4f ( goldenRatio , 0 , - 1 , 1 ) ,
190+ d . vec4f ( goldenRatio , 0 , 1 , 1 ) ,
191+ d . vec4f ( - goldenRatio , 0 , - 1 , 1 ) ,
192+ d . vec4f ( - goldenRatio , 0 , 1 , 1 ) ,
193+ ] . map ( ( v ) => {
194+ const length = Math . sqrt ( v . x * v . x + v . y * v . y + v . z * v . z ) ;
195+ return d . vec4f ( v . x / length , v . y / length , v . z / length , 1 ) ;
196+ } ) ;
197+
198+ // Define the 20 triangular faces of the icosahedron using vertex indices
199+ const faces : [ number , number , number ] [ ] = [
200+ // 5 faces around vertex 0
201+ [ 0 , 11 , 5 ] ,
202+ [ 0 , 5 , 1 ] ,
203+ [ 0 , 1 , 7 ] ,
204+ [ 0 , 7 , 10 ] ,
205+ [ 0 , 10 , 11 ] ,
206+ // 5 adjacent faces
207+ [ 1 , 5 , 9 ] ,
208+ [ 5 , 11 , 4 ] ,
209+ [ 11 , 10 , 2 ] ,
210+ [ 10 , 7 , 6 ] ,
211+ [ 7 , 1 , 8 ] ,
212+ // 5 faces around vertex 3
213+ [ 3 , 9 , 4 ] ,
214+ [ 3 , 4 , 2 ] ,
215+ [ 3 , 2 , 6 ] ,
216+ [ 3 , 6 , 8 ] ,
217+ [ 3 , 8 , 9 ] ,
218+ // 5 adjacent faces
219+ [ 4 , 9 , 5 ] ,
220+ [ 2 , 4 , 11 ] ,
221+ [ 6 , 2 , 10 ] ,
222+ [ 8 , 6 , 7 ] ,
223+ [ 9 , 8 , 1 ] ,
224+ ] ;
225+
226+ const vertices : d . Infer < typeof Vertex > [ ] = [ ] ;
227+
228+ for ( const [ i1 , i2 , i3 ] of faces ) {
229+ const v1 = initialVertices [ i1 ] ;
230+ const v2 = initialVertices [ i2 ] ;
231+ const v3 = initialVertices [ i3 ] ;
232+
233+ if ( smooth ) {
234+ vertices . push ( createVertex ( v1 , v1 ) ) ;
235+ vertices . push ( createVertex ( v2 , v2 ) ) ;
236+ vertices . push ( createVertex ( v3 , v3 ) ) ;
237+ } else {
238+ const edge1 = d . vec4f ( v2 . x - v1 . x , v2 . y - v1 . y , v2 . z - v1 . z , 0 ) ;
239+ const edge2 = d . vec4f ( v3 . x - v1 . x , v3 . y - v1 . y , v3 . z - v1 . z , 0 ) ;
240+ const faceNormal = normalizeSafely (
241+ d . vec4f (
242+ edge1 . y * edge2 . z - edge1 . z * edge2 . y ,
243+ edge1 . z * edge2 . x - edge1 . x * edge2 . z ,
244+ edge1 . x * edge2 . y - edge1 . y * edge2 . x ,
245+ 0 ,
246+ ) ,
247+ ) ;
248+ vertices . push ( createVertex ( v1 , faceNormal ) ) ;
249+ vertices . push ( createVertex ( v2 , faceNormal ) ) ;
250+ vertices . push ( createVertex ( v3 , faceNormal ) ) ;
251+ }
252+ }
253+
254+ return vertices ;
255+ }
256+
257+ const icoshpereCache = new Map <
258+ string ,
259+ TgpuBuffer < d . Disarray < typeof Vertex > > & VertexFlag
260+ > ( ) ;
261+
262+ export function createIcosphereShader (
263+ subdivisions : number ,
264+ smooth : boolean ,
265+ root : TgpuRoot ,
266+ ) : TgpuBuffer < d . Disarray < typeof Vertex > > & VertexFlag {
267+ const key = `${ subdivisions } -${ smooth } ` ;
268+ const cached = icoshpereCache . get ( key ) ;
269+ if ( cached ) {
270+ return cached ;
271+ }
272+
273+ const buffer = subdivide ( subdivisions , smooth , root ) ;
274+ icoshpereCache . set ( key , buffer ) ;
275+ return buffer ;
276+ }
277+
278+ function subdivide (
279+ wantedSubdivisions : number ,
280+ smooth : boolean ,
281+ root : TgpuRoot ,
282+ ) : TgpuBuffer < d . Disarray < typeof Vertex > > & VertexFlag {
283+ if ( wantedSubdivisions === 0 ) {
284+ const key = `${ wantedSubdivisions } -${ smooth } ` ;
285+ const cached = icoshpereCache . get ( key ) ;
286+ if ( cached ) {
287+ return cached ;
288+ }
289+
290+ const initialVertices = root
291+ . createBuffer (
292+ d . disarrayOf ( Vertex , getVertexAmount ( 0 ) ) ,
293+ createBaseIcosphere ( smooth ) ,
294+ )
295+ . $usage ( 'vertex' )
296+ . $addFlags ( GPUBufferUsage . STORAGE ) ;
297+ icoshpereCache . set ( key , initialVertices ) ;
298+ return initialVertices ;
299+ }
300+
301+ const previousKey = `${ wantedSubdivisions - 1 } -${ smooth } ` ;
302+ let previousVertices = icoshpereCache . get ( previousKey ) ;
303+ if ( ! previousVertices ) {
304+ previousVertices = subdivide ( wantedSubdivisions - 1 , smooth , root ) ;
305+ }
306+
307+ const nextBuffer = root
308+ . createBuffer ( d . disarrayOf ( Vertex , getVertexAmount ( wantedSubdivisions ) ) )
309+ . $usage ( 'vertex' )
310+ . $addFlags ( GPUBufferUsage . STORAGE ) ;
311+
312+ const currentComputeView = root
313+ . createBuffer (
314+ d . arrayOf ( ComputeVertex , getVertexAmount ( wantedSubdivisions - 1 ) ) ,
315+ previousVertices . buffer ,
316+ )
317+ . $usage ( 'storage' ) ;
318+ const nextComputeView = root
319+ . createBuffer (
320+ d . arrayOf ( ComputeVertex , getVertexAmount ( wantedSubdivisions ) ) ,
321+ nextBuffer . buffer ,
322+ )
323+ . $usage ( 'storage' ) ;
324+
325+ const smoothBuffer = root
326+ . createBuffer ( d . u32 , smooth ? 1 : 0 )
327+ . $usage ( 'uniform' ) ;
328+
329+ const bindGroupLayout = tgpu . bindGroupLayout ( {
330+ prevVertices : {
331+ storage : ( n : number ) => d . arrayOf ( ComputeVertex , n ) ,
332+ access : 'readonly' ,
333+ } ,
334+ nextVertices : {
335+ storage : ( n : number ) => d . arrayOf ( ComputeVertex , n ) ,
336+ access : 'mutable' ,
337+ } ,
338+ smoothFlag : { uniform : d . u32 } ,
339+ } ) ;
340+
341+ const bindGroup = root . createBindGroup ( bindGroupLayout , {
342+ prevVertices : currentComputeView ,
343+ nextVertices : nextComputeView ,
344+ smoothFlag : smoothBuffer ,
345+ } ) ;
346+
347+ const { prevVertices, nextVertices, smoothFlag } = bindGroupLayout . bound ;
348+
349+ const unpackVec2u = tgpu [ '~unstable' ] . fn ( [ d . vec2u ] , d . vec4f ) . does ( ( input ) => {
350+ const xy = std . unpack2x16float ( input . x ) ;
351+ const zw = std . unpack2x16float ( input . y ) ;
352+ return d . vec4f ( xy . x , xy . y , zw . x , zw . y ) ;
353+ } ) ;
354+
355+ const packVec2u = tgpu [ '~unstable' ] . fn ( [ d . vec4f ] , d . vec2u ) . does ( ( input ) => {
356+ const xy = std . pack2x16float ( d . vec2f ( input . x , input . y ) ) ;
357+ const zw = std . pack2x16float ( d . vec2f ( input . z , input . w ) ) ;
358+ return d . vec2u ( xy , zw ) ;
359+ } ) ;
360+
361+ const getNormal = tgpu [ '~unstable' ]
362+ . fn ( [ d . vec4f , d . vec4f , d . vec4f , d . u32 , d . vec4f ] , d . vec4f )
363+ . does ( ( v1 , v2 , v3 , smooth , vertexPos ) => {
364+ if ( smooth === 1 ) {
365+ // For smooth shading on a sphere, the normal is the same as the normalized position
366+ return vertexPos ;
367+ }
368+ const edge1 = d . vec4f ( v2 . x - v1 . x , v2 . y - v1 . y , v2 . z - v1 . z , 0 ) ;
369+ const edge2 = d . vec4f ( v3 . x - v1 . x , v3 . y - v1 . y , v3 . z - v1 . z , 0 ) ;
370+ return std . normalize (
371+ d . vec4f (
372+ edge1 . y * edge2 . z - edge1 . z * edge2 . y ,
373+ edge1 . z * edge2 . x - edge1 . x * edge2 . z ,
374+ edge1 . x * edge2 . y - edge1 . y * edge2 . x ,
375+ 0 ,
376+ ) ,
377+ ) ;
378+ } ) ;
379+
380+ const calculateMidpoint = tgpu [ '~unstable' ]
381+ . fn ( [ d . vec4f , d . vec4f ] , d . vec4f )
382+ . does ( ( v1 , v2 ) => {
383+ return d . vec4f (
384+ ( v1 . x + v2 . x ) * 0.5 ,
385+ ( v1 . y + v2 . y ) * 0.5 ,
386+ ( v1 . z + v2 . z ) * 0.5 ,
387+ 1 ,
388+ ) ;
389+ } ) ;
390+
391+ const normalizeSafely = tgpu [ '~unstable' ] . fn ( [ d . vec4f ] , d . vec4f ) . does ( ( v ) => {
392+ const length = std . length ( d . vec3f ( v . x , v . y , v . z ) ) ;
393+ const epsilon = 1e-8 ;
394+ if ( length < epsilon ) {
395+ return d . vec4f ( 0 , 0 , 1 , 1 ) ;
396+ }
397+ return d . vec4f ( v . x / length , v . y / length , v . z / length , 1 ) ;
398+ } ) ;
399+
400+ const computeFn = tgpu [ '~unstable' ]
401+ . computeFn ( {
402+ in : { gid : d . builtin . globalInvocationId } ,
403+ workgroupSize : [ 64 , 1 , 1 ] ,
404+ } )
405+ . does ( ( input ) => {
406+ const triangleCount = std . arrayLength ( prevVertices . value ) / d . u32 ( 3 ) ;
407+ // Calculate global triangleIndex from 2D dispatch
408+ const triangleIndex = input . gid . x + input . gid . y * d . u32 ( 65535 ) ;
409+ if ( triangleIndex >= triangleCount ) {
410+ return ;
411+ }
412+
413+ const baseIndexPrev = triangleIndex * d . u32 ( 3 ) ;
414+
415+ // Read the 3 vertices of the triangle
416+ const v1 = unpackVec2u ( prevVertices . value [ baseIndexPrev ] . position ) ;
417+ const v2 = unpackVec2u (
418+ prevVertices . value [ baseIndexPrev + d . u32 ( 1 ) ] . position ,
419+ ) ;
420+ const v3 = unpackVec2u (
421+ prevVertices . value [ baseIndexPrev + d . u32 ( 2 ) ] . position ,
422+ ) ;
423+
424+ // Calculate the midpoints of the edges and reproject them onto the unit sphere
425+ const v12 = normalizeSafely ( calculateMidpoint ( v1 , v2 ) ) ;
426+ const v23 = normalizeSafely ( calculateMidpoint ( v2 , v3 ) ) ;
427+ const v31 = normalizeSafely ( calculateMidpoint ( v3 , v1 ) ) ;
428+
429+ const newVertices = [
430+ // Triangle A: [v1, v12, v31]
431+ v1 ,
432+ v12 ,
433+ v31 ,
434+ // Triangle B: [v2, v23, v12]
435+ v2 ,
436+ v23 ,
437+ v12 ,
438+ // Triangle C: [v3, v31, v23]
439+ v3 ,
440+ v31 ,
441+ v23 ,
442+ // Triangle D: [v12, v23, v31]
443+ v12 ,
444+ v23 ,
445+ v31 ,
446+ ] ;
447+
448+ const baseIndexNext = triangleIndex * d . u32 ( 12 ) ;
449+ // For each of the 12 new vertices, compute and store their values.
450+ for ( let i = d . u32 ( 0 ) ; i < 12 ; i ++ ) {
451+ const reprojectedVertex = newVertices [ i ] ;
452+
453+ const triBase = i - ( i % d . u32 ( 3 ) ) ;
454+ const normal = getNormal (
455+ newVertices [ triBase ] ,
456+ newVertices [ triBase + d . u32 ( 1 ) ] ,
457+ newVertices [ triBase + d . u32 ( 2 ) ] ,
458+ smoothFlag . value ,
459+ reprojectedVertex ,
460+ ) ;
461+ const outIndex = baseIndexNext + i ;
462+ const nextVertex = nextVertices . value [ outIndex ] ;
463+ nextVertex . position = packVec2u ( reprojectedVertex ) ;
464+ nextVertex . normal = packVec2u ( normal ) ;
465+ nextVertices . value [ outIndex ] = nextVertex ;
466+ }
467+ } ) ;
468+
469+ const pipeline = root [ '~unstable' ] . withCompute ( computeFn ) . createPipeline ( ) ;
470+
471+ // Calculate the appropriate workgroup dispatch dimensions, splitting across X and Y
472+ // when needed to stay within the 65535 limit
473+ const triangleCount = getVertexAmount ( wantedSubdivisions - 1 ) / 3 ;
474+ const xGroups = Math . min ( triangleCount , 65535 ) ;
475+ const yGroups = Math . ceil ( triangleCount / 65535 ) ;
476+
477+ pipeline
478+ . with ( bindGroupLayout , bindGroup )
479+ . dispatchWorkgroups ( xGroups , yGroups , 1 ) ;
480+
481+ root [ '~unstable' ] . flush ( ) ;
482+
483+ return nextBuffer ;
484+ }
0 commit comments