1- import tgpu from 'typegpu' ;
1+ import * as sdf from '@typegpu/sdf' ;
2+ import tgpu , {
3+ type SampledFlag ,
4+ type StorageFlag ,
5+ type TgpuTexture ,
6+ } from 'typegpu' ;
27import { fullScreenTriangle } from 'typegpu/common' ;
38import * as d from 'typegpu/data' ;
49import * as std from 'typegpu/std' ;
5- import * as sdf from '@typegpu/sdf ' ;
10+ import { DragController } from './drag-controller.ts ' ;
611import {
712 SceneData ,
813 sceneData ,
914 sceneDataAccess ,
1015 sceneSDF ,
1116 updateElementPosition ,
1217} from './scene.ts' ;
13- import { DragController } from './drag-controller.ts' ;
1418
1519const root = await tgpu . init ( ) ;
1620const canvas = document . querySelector ( 'canvas' ) as HTMLCanvasElement ;
@@ -23,7 +27,7 @@ context.configure({
2327} ) ;
2428
2529const OUTPUT_RESOLUTION : [ number , number ] = [ canvas . width , canvas . height ] ;
26- const LIGHTING_RESOLUTION = 0.4 ;
30+ const LIGHTING_RESOLUTION = 0.35 ;
2731
2832const [ outputProbesX , outputProbesY ] = OUTPUT_RESOLUTION ;
2933const aspect = outputProbesX / outputProbesY ;
@@ -37,28 +41,33 @@ const cascadeProbesX = aspect >= 1
3741const cascadeProbesY = aspect >= 1
3842 ? cascadeProbesMin
3943 : Math . round ( cascadeProbesMin / aspect ) ;
40- const cascadeDimX = cascadeProbesX * 2 ; // 2x2 stored rays per probe
44+ const cascadeDimX = cascadeProbesX * 2 ;
4145const cascadeDimY = cascadeProbesY * 2 ;
4246
4347const interval0 = 1 / cascadeProbesMin ;
44- const maxIntervalStart = 1.5 ; // ~diagonal in UV space
48+ const maxIntervalStart = 1.5 ;
4549const cascadeAmount = Math . ceil (
46- Math . log2 ( maxIntervalStart * 3 / interval0 + 1 ) / 2 ,
50+ Math . log2 ( ( maxIntervalStart * 3 ) / interval0 + 1 ) / 2 ,
4751) ;
4852
49- const cascadesTextureA = root [ '~unstable' ]
50- . createTexture ( {
51- size : [ cascadeDimX , cascadeDimY , cascadeAmount ] ,
52- format : 'rgba16float' ,
53- } )
54- . $usage ( 'storage' , 'sampled' ) ;
55-
56- const cascadesTextureB = root [ '~unstable' ]
57- . createTexture ( {
58- size : [ cascadeDimX , cascadeDimY , cascadeAmount ] ,
59- format : 'rgba16float' ,
60- } )
61- . $usage ( 'storage' , 'sampled' ) ;
53+ type CascadeTexture =
54+ & TgpuTexture < {
55+ size : [ number , number , number ] ;
56+ format : 'rgba16float' ;
57+ } >
58+ & StorageFlag
59+ & SampledFlag ;
60+
61+ const cascadeTextures = Array . from (
62+ { length : 2 } ,
63+ ( ) =>
64+ root [ '~unstable' ]
65+ . createTexture ( {
66+ size : [ cascadeDimX , cascadeDimY , cascadeAmount ] ,
67+ format : 'rgba16float' ,
68+ } )
69+ . $usage ( 'storage' , 'sampled' ) ,
70+ ) as [ CascadeTexture , CascadeTexture ] ;
6271
6372const radianceFieldTex = root [ '~unstable' ]
6473 . createTexture ( {
@@ -128,7 +137,9 @@ const cascadePassCompute = tgpu['~unstable'].computeFn({
128137 in : { gid : d . builtin . globalInvocationId } ,
129138} ) ( ( { gid } ) => {
130139 const dim2 = cascadeDimUniform . $ ;
131- if ( gid . x >= dim2 . x || gid . y >= dim2 . y ) return ;
140+ if ( gid . x >= dim2 . x || gid . y >= dim2 . y ) {
141+ return ;
142+ }
132143
133144 const layer = cascadeIndexUniform . $ ;
134145 const probes = probesUniform . $ ;
@@ -156,7 +167,7 @@ const cascadePassCompute = tgpu['~unstable'].computeFn({
156167 const cascadeProbesMinVal = d . f32 ( std . min ( cascadeProbes . x , cascadeProbes . y ) ) ;
157168 const interval0 = 1.0 / cascadeProbesMinVal ;
158169 const pow4 = d . f32 ( d . u32 ( 1 ) << ( layer * d . u32 ( 2 ) ) ) ;
159- const startUv = interval0 * ( pow4 - 1.0 ) / 3.0 ;
170+ const startUv = ( interval0 * ( pow4 - 1.0 ) ) / 3.0 ;
160171 const endUv = startUv + interval0 * pow4 ;
161172 const eps = 0.5 / cascadeProbesMinVal ;
162173 const minStep = 0.25 / cascadeProbesMinVal ;
@@ -165,9 +176,9 @@ const cascadePassCompute = tgpu['~unstable'].computeFn({
165176
166177 // Cast 4 rays per stored texel (2x2 block) and average
167178 for ( let i = 0 ; i < 4 ; i ++ ) {
168- const dirActual = dirStored . mul ( d . u32 ( 2 ) ) . add (
169- d . vec2u ( d . u32 ( i ) & d . u32 ( 1 ) , d . u32 ( i ) >> d . u32 ( 1 ) ) ,
170- ) ;
179+ const dirActual = dirStored
180+ . mul ( d . u32 ( 2 ) )
181+ . add ( d . vec2u ( d . u32 ( i ) & d . u32 ( 1 ) , d . u32 ( i ) >> d . u32 ( 1 ) ) ) ;
171182 const rayIndex = d . f32 ( dirActual . y * raysDimActual + dirActual . x ) + 0.5 ;
172183 const angle = ( rayIndex / d . f32 ( rayCountActual ) ) * ( Math . PI * 2 ) - Math . PI ;
173184 const rayDir = d . vec2f ( std . cos ( angle ) , - std . sin ( angle ) ) ;
@@ -284,7 +295,10 @@ const buildRadianceFieldCompute = tgpu['~unstable'].computeFn({
284295 ) ;
285296} ) ;
286297
287- const ACESFilm = tgpu . fn ( [ d . vec3f ] , d . vec3f ) ( ( x ) => {
298+ const ACESFilm = tgpu . fn (
299+ [ d . vec3f ] ,
300+ d . vec3f ,
301+ ) ( ( x ) => {
288302 const a = 2.51 ;
289303 const b = 0.03 ;
290304 const c = 2.43 ;
@@ -298,8 +312,11 @@ const finalRadianceFieldFrag = tgpu['~unstable'].fragmentFn({
298312 in : { uv : d . vec2f } ,
299313 out : d . vec4f ,
300314} ) ( ( { uv } ) => {
301- const field = std . textureSample ( radianceFieldView . $ , radianceSampler . $ , uv )
302- . xyz ;
315+ const field = std . textureSample (
316+ radianceFieldView . $ ,
317+ radianceSampler . $ ,
318+ uv ,
319+ ) . xyz ;
303320 const outRgb = std . saturate ( field ) ;
304321 return d . vec4f ( ACESFilm ( outRgb ) , 1.0 ) ;
305322} ) ;
@@ -318,7 +335,7 @@ const overlayFrag = tgpu['~unstable'].fragmentFn({
318335 const baseColor = ACESFilm ( std . saturate ( field ) ) ;
319336
320337 if ( overlayEnabledUniform . $ === d . u32 ( 0 ) ) {
321- return d . vec4f ( baseColor , 1.0 ) ;
338+ return d . vec4f ( baseColor , 1 ) ;
322339 }
323340
324341 const debugLayer = overlayDebugCascadeUniform . $ ;
@@ -335,17 +352,17 @@ const overlayFrag = tgpu['~unstable'].fragmentFn({
335352
336353 // Interval for ray visualization
337354 const cascadeProbesMinVal = d . f32 ( std . min ( cascadeProbes . x , cascadeProbes . y ) ) ;
338- const interval0 = 1.0 / cascadeProbesMinVal ;
355+ const interval0 = 1 / cascadeProbesMinVal ;
339356 const pow4 = d . f32 ( d . u32 ( 1 ) << ( debugLayer * d . u32 ( 2 ) ) ) ;
340- const endUv = interval0 * ( pow4 - 1.0 ) / 3.0 + interval0 * pow4 ;
357+ const endUv = ( interval0 * ( pow4 - 1 ) ) / 3 + interval0 * pow4 ;
341358
342359 // Visual parameters
343- const probeSpacing = std . min ( 1.0 / d . f32 ( probes . x ) , 1.0 / d . f32 ( probes . y ) ) ;
360+ const probeSpacing = std . min ( 1 / d . f32 ( probes . x ) , 1 / d . f32 ( probes . y ) ) ;
344361 const probeRadius = std . max ( probeSpacing * 0.08 , 0.002 ) ;
345362 const rayThickness = std . max ( probeSpacing * 0.03 , 0.001 ) ;
346363
347- let minProbeDist = d . f32 ( 1000.0 ) ;
348- let minRayDist = d . f32 ( 1000.0 ) ;
364+ let minProbeDist = d . f32 ( 1000 ) ;
365+ let minRayDist = d . f32 ( 1000 ) ;
349366 let closestRayColor = d . vec3f ( ) ;
350367
351368 const centerProbe = d . vec2i ( std . floor ( uv . mul ( d . vec2f ( probes ) ) ) ) ;
@@ -355,9 +372,13 @@ const overlayFrag = tgpu['~unstable'].fragmentFn({
355372 for ( let px = - 1 ; px <= 1 ; px ++ ) {
356373 const probeXY = centerProbe . add ( d . vec2i ( px , py ) ) ;
357374 if (
358- probeXY . x < 0 || probeXY . x >= d . i32 ( probes . x ) || probeXY . y < 0 ||
375+ probeXY . x < 0 ||
376+ probeXY . x >= d . i32 ( probes . x ) ||
377+ probeXY . y < 0 ||
359378 probeXY . y >= d . i32 ( probes . y )
360- ) continue ;
379+ ) {
380+ continue ;
381+ }
361382
362383 const probe = d . vec2u ( probeXY ) ;
363384 const probePos = d . vec2f ( probe ) . add ( 0.5 ) . div ( d . vec2f ( probes ) ) ;
@@ -366,15 +387,15 @@ const overlayFrag = tgpu['~unstable'].fragmentFn({
366387 sdf . sdDisk ( uv . sub ( probePos ) , probeRadius ) ,
367388 ) ;
368389
369- // Only draw rays near probe
370- if ( std . length ( uv . sub ( probePos ) ) > probeSpacing * 0.7 ) continue ;
390+ if ( std . length ( uv . sub ( probePos ) ) > probeSpacing * 0.7 ) {
391+ continue ;
392+ }
371393
372- // Sample subset of rays
373- const rayStep = std . max ( d . u32 ( 1 ) , rayCountActual / d . u32 ( 24 ) ) ;
394+ const rayStep = std . max ( 1 , d . u32 ( rayCountActual / 24 ) ) ;
374395 let ri = d . u32 ( 0 ) ;
375396 while ( ri < rayCountActual ) {
376397 const rayIndex = d . f32 ( ri ) + 0.5 ;
377- const angle = ( rayIndex / d . f32 ( rayCountActual ) ) * ( Math . PI * 2 ) -
398+ const angle = ( rayIndex / rayCountActual ) * ( Math . PI * 2 ) -
378399 Math . PI ;
379400 const rayDir = d . vec2f ( std . cos ( angle ) , - std . sin ( angle ) ) ;
380401 const rayDist = sdf . sdLine (
@@ -434,8 +455,8 @@ const cascadePassBindGroups = Array.from(
434455 { length : cascadeAmount } ,
435456 ( _ , layer ) => {
436457 const writeToA = ( cascadeAmount - 1 - layer ) % 2 === 0 ;
437- const dstTexture = writeToA ? cascadesTextureA : cascadesTextureB ;
438- const srcTexture = writeToA ? cascadesTextureB : cascadesTextureA ;
458+ const dstTexture = cascadeTextures [ writeToA ? 0 : 1 ] ;
459+ const srcTexture = cascadeTextures [ writeToA ? 1 : 0 ] ;
439460
440461 return root . createBindGroup ( cascadePassBGL , {
441462 upper : srcTexture . createView ( d . texture2d ( d . f32 ) , {
@@ -455,30 +476,25 @@ const buildRadianceFieldPipeline = root['~unstable']
455476 . withCompute ( buildRadianceFieldCompute )
456477 . createPipeline ( ) ;
457478
458- const buildRadianceFieldBG_A = root . createBindGroup ( buildRadianceFieldBGL , {
459- src : cascadesTextureA . createView ( d . texture2d ( d . f32 ) , {
460- baseArrayLayer : 0 ,
461- arrayLayerCount : 1 ,
462- } ) ,
463- srcSampler : cascadeSampler ,
464- dst : radianceFieldStoreView ,
465- } ) ;
466-
467- const buildRadianceFieldBG_B = root . createBindGroup ( buildRadianceFieldBGL , {
468- src : cascadesTextureB . createView ( d . texture2d ( d . f32 ) , {
469- baseArrayLayer : 0 ,
470- arrayLayerCount : 1 ,
471- } ) ,
472- srcSampler : cascadeSampler ,
473- dst : radianceFieldStoreView ,
474- } ) ;
479+ const createBuildRadianceFieldBG = ( textureIndex : number ) =>
480+ root . createBindGroup ( buildRadianceFieldBGL , {
481+ src : cascadeTextures [ textureIndex ] . createView ( d . texture2d ( d . f32 ) , {
482+ baseArrayLayer : 0 ,
483+ arrayLayerCount : 1 ,
484+ } ) ,
485+ srcSampler : cascadeSampler ,
486+ dst : radianceFieldStoreView ,
487+ } ) ;
488+
489+ const buildRadianceFieldBindGroups = [
490+ createBuildRadianceFieldBG ( 0 ) ,
491+ createBuildRadianceFieldBG ( 1 ) ,
492+ ] ;
475493
476494function buildRadianceField ( ) {
477- // Determine which texture has cascade 0 after ping-pong
478495 const cascade0InA = ( cascadeAmount - 1 ) % 2 === 0 ;
479- const buildRadianceFieldBG = cascade0InA
480- ? buildRadianceFieldBG_A
481- : buildRadianceFieldBG_B ;
496+ const buildRadianceFieldBG =
497+ buildRadianceFieldBindGroups [ cascade0InA ? 0 : 1 ] ;
482498
483499 buildRadianceFieldPipeline
484500 . with ( buildRadianceFieldBG )
@@ -506,36 +522,35 @@ function runCascadesTopDown() {
506522}
507523
508524function updateLighting ( ) {
509- runCascadesTopDown ( ) ; // Fused raymarch + merge
510- buildRadianceField ( ) ; // Build final radiance field from cascade 0
525+ runCascadesTopDown ( ) ;
526+ buildRadianceField ( ) ;
511527}
512528updateLighting ( ) ;
513529
514- // Create bind groups for overlay debug - need both A and B textures for ping-pong
515- const overlayDebugBG_A = root . createBindGroup ( overlayDebugBGL , {
516- cascadeTex : cascadesTextureA . createView ( d . texture2dArray ( d . f32 ) ) ,
517- cascadeSampler : cascadeSampler ,
518- } ) ;
530+ const createOverlayDebugBG = ( textureIndex : number ) =>
531+ root . createBindGroup ( overlayDebugBGL , {
532+ cascadeTex : cascadeTextures [ textureIndex ] . createView (
533+ d . texture2dArray ( d . f32 ) ,
534+ ) ,
535+ cascadeSampler : cascadeSampler ,
536+ } ) ;
519537
520- const overlayDebugBG_B = root . createBindGroup ( overlayDebugBGL , {
521- cascadeTex : cascadesTextureB . createView ( d . texture2dArray ( d . f32 ) ) ,
522- cascadeSampler : cascadeSampler ,
523- } ) ;
538+ const overlayDebugBindGroups = [
539+ createOverlayDebugBG ( 0 ) ,
540+ createOverlayDebugBG ( 1 ) ,
541+ ] ;
524542
525543const renderPipeline = root [ '~unstable' ]
526544 . withVertex ( fullScreenTriangle )
527545 . withFragment ( overlayFrag , { format : presentationFormat } )
528546 . createPipeline ( ) ;
529547
530- let isRunning = true ;
531548let frameId : number ;
532549let debugLayer = 0 ;
533550
534551async function frame ( ) {
535- if ( ! isRunning ) return ; // Prevent using destroyed device
536-
537552 const writeToA = ( cascadeAmount - 1 - debugLayer ) % 2 === 0 ;
538- const overlayDebugBG = writeToA ? overlayDebugBG_A : overlayDebugBG_B ;
553+ const overlayDebugBG = overlayDebugBindGroups [ writeToA ? 0 : 1 ] ;
539554
540555 renderPipeline
541556 . with ( overlayDebugBG )
@@ -553,7 +568,6 @@ function updateUniforms() {
553568 sceneDataUniform . write ( sceneData ) ;
554569}
555570
556- // Set up drag controller for interactive scene manipulation
557571const dragController = new DragController (
558572 canvas ,
559573 ( id , position ) => {
@@ -569,7 +583,6 @@ const dragController = new DragController(
569583) ;
570584
571585export function onCleanup ( ) {
572- isRunning = false ; // Stop the loop logic immediately
573586 dragController . destroy ( ) ;
574587 if ( frameId !== null ) {
575588 cancelAnimationFrame ( frameId ) ;
0 commit comments