1+ import * as twgl from "twgl.js"
2+
3+ const CENTER = twgl . v3 . create ( 0 , 0 , 0 ) ;
4+ const EPSILON = 1e-6 ; // from raymath
5+
6+ const CUBE_SIZE = 1.0 ;
7+ const CUBE_PADDING = 0.1 ;
8+
9+ const CUBES_X = 25 ;
10+ const CUBES_Y = 25 ;
11+ const CUBES_Z = 25 ;
12+ const CUBES_COUNT = ( CUBES_X * CUBES_Y * CUBES_Z ) ;
13+
14+ // note: SIZE_X is the scaling factor for bullet speed, radius, etc.
15+ const GETLENGTH = ( x : number ) => ( ( CUBE_SIZE + CUBE_PADDING ) * ( x ) - CUBE_PADDING ) ;
16+ const SIZE_X = GETLENGTH ( CUBES_X ) ;
17+ const SIZE_Y = GETLENGTH ( CUBES_Y ) ;
18+ const SIZE_Z = GETLENGTH ( CUBES_Z ) ;
19+
20+ const X_MIN = ( CENTER [ 0 ] - SIZE_X / 2.0 ) ;
21+ const X_MAX = ( CENTER [ 0 ] + SIZE_X / 2.0 ) ;
22+ const Y_MIN = ( CENTER [ 1 ] - SIZE_Y / 2.0 ) ;
23+ const Y_MAX = ( CENTER [ 1 ] + SIZE_Y / 2.0 ) ;
24+ const Z_MIN = ( CENTER [ 2 ] - SIZE_Z / 2.0 ) ;
25+ const Z_MAX = ( CENTER [ 2 ] + SIZE_Z / 2.0 ) ;
26+
27+ const X_MIN_CUBE_CENTER = ( X_MIN + CUBE_SIZE / 2.0 ) ;
28+ const X_MAX_CUBE_CENTER = ( X_MAX - CUBE_SIZE / 2.0 ) ;
29+ const Y_MIN_CUBE_CENTER = ( Y_MIN + CUBE_SIZE / 2.0 ) ;
30+ const Y_MAX_CUBE_CENTER = ( Y_MAX - CUBE_SIZE / 2.0 ) ;
31+ const Z_MIN_CUBE_CENTER = ( Z_MIN + CUBE_SIZE / 2.0 ) ;
32+ const Z_MAX_CUBE_CENTER = ( Z_MAX - CUBE_SIZE / 2.0 ) ;
33+
34+ const CUBE_IDX = ( x : number , y : number , z : number ) => CUBES_X * CUBES_Y * z + CUBES_X * y + x ;
35+ const GET_X = ( i : number ) => 3 * i ;
36+ const GET_Y = ( i : number ) => 3 * i + 1 ;
37+ const GET_Z = ( i : number ) => 3 * i + 2 ;
38+ const getVec3Indices = ( i : number ) => {
39+ const base = 3 * i ;
40+ return { x : base , y : base + 1 , z : base + 2 } ;
41+ }
42+
43+ const BULLET_COUNT = 5 ;
44+ const LIST_END = - 1 ;
45+ const IS_SPAWNED = - 2 ;
46+
47+ // --------------------------------------------
48+
49+ const Direction = {
50+ PX : 0x01 ,
51+ NX : 0x02 ,
52+ PY : 0x04 ,
53+ NY : 0x08 ,
54+ PZ : 0x10 ,
55+ NZ : 0x20 ,
56+ LEN : 6
57+ } as const ;
58+
59+ type DirVals = typeof Direction [ keyof typeof Direction ] ;
60+
61+ type Bullets = {
62+ positions : Float32Array ,
63+ colors : Float32Array ,
64+ scales : Float32Array ,
65+ speeds : number [ ] ,
66+ next_free_or_spawned : number [ ] ,
67+ directions : DirVals [ ]
68+ } ;
69+
70+ type Freelist = { head : number , tail : number }
71+
72+ // ------------------------------------------
73+
74+ // todo: maybe port over the xorshift if this isn't giving good distributions
75+ const nextRand = ( min : number , max : number ) => min + ( max - min ) * Math . random ( ) ;
76+
77+ const getRandomGridPos = ( count : number , base : number ) => {
78+ throw new Error ( "unimplemented" ) ;
79+ return 0 ; // todo: implement
80+ } ;
81+
82+ const getActiveXyzIdx = ( dir : DirVals , idx : number ) => {
83+ let res = idx * 3 ;
84+ if ( dir & ( Direction . PY | Direction . NY ) ) res += 1 ;
85+ else if ( dir & ( Direction . PZ | Direction . NZ ) ) res += 2 ;
86+ return res ;
87+ } ;
88+
89+ const getSign = ( dir : DirVals ) => {
90+ return dir & ( Direction . PX | Direction . PY | Direction . PZ ) ? 1 : - 1 ;
91+ } ;
92+
93+ // ----------------------------------------
94+
95+ const initFreelist = ( frie : Freelist , bullets : Bullets ) => {
96+ for ( let i = 0 ; i < BULLET_COUNT - 1 ; i ++ )
97+ bullets . next_free_or_spawned [ i ] = i + 1 ;
98+ bullets . next_free_or_spawned [ BULLET_COUNT - 1 ] = LIST_END ;
99+ frie . head = 0 ;
100+ frie . tail = BULLET_COUNT - 1 ;
101+ } ;
102+
103+ const spawnBullet = ( freelist : Freelist , bullets : Bullets ) => {
104+ if ( freelist . head == LIST_END ) return LIST_END ;
105+ const idx = freelist . head ;
106+ freelist . head = bullets . next_free_or_spawned [ idx ] ;
107+ if ( freelist . head == LIST_END )
108+ freelist . tail = LIST_END ;
109+ bullets . next_free_or_spawned [ idx ] = IS_SPAWNED ;
110+
111+ const color = twgl . v3 . lerp ( [ 0xC7 / 255 , 0x51 / 255 , 0x08 / 255 ] , [ 0x61 / 255 , 0x0C / 255 , 0xCF / 255 ] , Math . random ( ) ) ;
112+ const bx = idx * 3 ;
113+ const by = bx + 1 ;
114+ const bz = by + 1 ;
115+ bullets . colors [ bx ] = color [ 0 ] ;
116+ bullets . colors [ by ] = color [ 1 ] ;
117+ bullets . colors [ bz ] = color [ 2 ] ;
118+ bullets . directions [ idx ] = ( 1 << ( Math . floor ( Math . random ( ) * Direction . LEN ) ) ) as DirVals ;
119+ bullets . speeds [ idx ] = nextRand ( 0 , 1 ) ; // todo: constants
120+ const bulletRadius = nextRand ( 0 , 1 ) ; // todo: constants
121+ bullets . scales [ bx ] = bulletRadius ;
122+ bullets . scales [ by ] = bulletRadius ;
123+ bullets . scales [ bz ] = bulletRadius ;
124+ bullets . positions [ bx ] = getRandomGridPos ( CUBES_X , X_MIN_CUBE_CENTER ) ;
125+ bullets . positions [ by ] = getRandomGridPos ( CUBES_Y , Y_MIN_CUBE_CENTER ) ;
126+ bullets . positions [ bz ] = getRandomGridPos ( CUBES_Z , Z_MIN_CUBE_CENTER ) ;
127+ const activeIdx = getActiveXyzIdx ( bullets . directions [ idx ] , idx ) ;
128+ bullets . scales [ activeIdx ] = nextRand ( 0 , 1 ) ; // todo: constants
129+ bullets . positions [ activeIdx ] = nextRand ( 0 , 1 ) ; // todo: constants
130+ return idx ;
131+ } ;
132+
133+ const freeBullet = ( freelist : Freelist , bullets : Bullets , idx : number ) => {
134+ // todo: implement
135+ throw new Error ( "unimplemented" ) ;
136+ } ;
137+
138+ const worldToIndex = ( coord : number , basePos : number , maxIdx : number ) => {
139+ // NOTE: idk why ceilf had to be used here but it fixed an off by -1 offset
140+ // issue.
141+ const ret = Math . ceil ( ( coord - basePos ) / ( CUBE_SIZE + CUBE_PADDING ) ) ;
142+ return ret > maxIdx ? maxIdx : ret < 0 ? 0 : ret ;
143+ }
144+
145+ const getBulletBoundingBox = ( bullets : Bullets , idx : number ) => {
146+ const vIdx = getVec3Indices ( i ) ;
147+ return {
148+ min_x : worldToIndex ( bullets . positions [ vIdx . x ] - bullets . scales [ vIdx . x ] , X_MIN_CUBE_CENTER , CUBES_X ) ,
149+ max_x : worldToIndex ( bullets . positions [ vIdx . x ] + bullets . scales [ vIdx . x ] , X_MIN_CUBE_CENTER , CUBES_X ) ,
150+ min_y : worldToIndex ( bullets . positions [ vIdx . y ] - bullets . scales [ vIdx . y ] , Y_MIN_CUBE_CENTER , CUBES_Y ) ,
151+ max_y : worldToIndex ( bullets . positions [ vIdx . y ] + bullets . scales [ vIdx . y ] , Y_MIN_CUBE_CENTER , CUBES_Y ) ,
152+ min_z : worldToIndex ( bullets . positions [ vIdx . z ] - bullets . scales [ vIdx . z ] , Z_MIN_CUBE_CENTER , CUBES_Z ) ,
153+ max_z : worldToIndex ( bullets . positions [ vIdx . z ] + bullets . scales [ vIdx . z ] , Z_MIN_CUBE_CENTER , CUBES_Z ) ,
154+ } ;
155+ }
156+
157+ const isOutOfBounds = ( bullets : Bullets , idx : number ) => {
158+ const vIdx = getVec3Indices ( idx ) ;
159+ switch ( bullets . directions [ idx ] ) {
160+ case Direction . PX :
161+ return bullets . positions [ vIdx . x ] > ( X_MAX + bullets . scales [ vIdx . x ] ) ;
162+ case Direction . NX :
163+ return bullets . positions [ vIdx . x ] < ( X_MIN - bullets . scales [ vIdx . x ] ) ;
164+ case Direction . PY :
165+ return bullets . positions [ vIdx . y ] > ( Y_MAX + bullets . scales [ vIdx . y ] ) ;
166+ case Direction . NY :
167+ return bullets . positions [ vIdx . y ] < ( Y_MIN - bullets . scales [ vIdx . y ] ) ;
168+ case Direction . PZ :
169+ return bullets . positions [ vIdx . y ] > ( Z_MAX + bullets . scales [ vIdx . z ] ) ;
170+ case Direction . NZ :
171+ return bullets . positions [ vIdx . z ] < ( Z_MIN - bullets . scales [ vIdx . z ] ) ;
172+ default :
173+ return true ;
174+ }
175+ } ;
176+
177+ // ------------ Raylib ports ---------------
178+
179+ type Vector3 = { x : number , y : number , z : number } ;
180+ type Camera = {
181+ position : Vector3
182+ target : Vector3
183+ up : Vector3
184+ fovy : number
185+ projection : number // could probably get rid of this, only ever use CAMERA_PERSPECTIVE
186+ } ;
187+
188+ const UpdateCamera = ( camera : Camera ) => {
189+
190+ } ;
191+
192+ // -----------------------------------------
193+
194+ const main = ( ) => {
195+ const gl = ( document . getElementById ( "bg-canvas" ) as HTMLCanvasElement ) . getContext ( "webgl2" ) ;
196+ if ( ! gl ) return ;
197+ const bullets : Bullets = {
198+ positions : new Float32Array ( BULLET_COUNT * 3 ) ,
199+ colors : new Float32Array ( BULLET_COUNT * 3 ) ,
200+ scales : new Float32Array ( BULLET_COUNT * 3 ) ,
201+ speeds : new Array < number > ( BULLET_COUNT ) ,
202+ next_free_or_spawned : new Array < number > ( BULLET_COUNT ) ,
203+ directions : new Array < DirVals > ( BULLET_COUNT )
204+ } ;
205+ const freelist : Freelist = { head : LIST_END , tail : LIST_END } ;
206+ initFreelist ( freelist , bullets ) ;
207+
208+ const cubePositions = new Float32Array ( CUBES_COUNT * 3 ) ;
209+ const refPos = twgl . v3 . create ( 0 , 0 , Z_MIN_CUBE_CENTER ) ;
210+ for ( let z = 0 ; z < CUBES_Z ; z ++ ) {
211+ refPos [ 1 ] = Y_MIN_CUBE_CENTER ;
212+ for ( let y = 0 ; y < CUBES_Y ; y ++ ) {
213+ refPos [ 0 ] = X_MIN_CUBE_CENTER ;
214+ for ( let x = 0 ; x < CUBES_X ; x ++ ) {
215+ const idx = getVec3Indices ( CUBE_IDX ( x , y , z ) ) ;
216+ cubePositions [ idx . x ] = refPos [ 0 ] ; // x
217+ cubePositions [ idx . y ] = refPos [ 1 ] ; // y
218+ cubePositions [ idx . z ] = refPos [ 2 ] ; // z
219+ refPos [ 0 ] += CUBE_SIZE + CUBE_PADDING ;
220+ }
221+ refPos [ 1 ] += CUBE_SIZE + CUBE_PADDING ;
222+ }
223+ refPos [ 2 ] += CUBE_SIZE + CUBE_PADDING ;
224+ }
225+ // todo: upload cubePositions to VAO.
226+
227+ // todo: constants
228+ let spawnTimer = nextRand ( 0 , 1 ) ;
229+
230+ // todo/architecture: pass around "base index" (pointing to x component) instead of doing getVec3Indices everywhere
231+ const render = ( dt : number ) => {
232+ if ( ( spawnTimer -= dt ) <= 0 ) {
233+ spawnBullet ( freelist , bullets ) ;
234+ spawnTimer = nextRand ( 0 , 1 ) ; // todo: constants
235+ }
236+
237+ // todo: UpdateCamera (orbital)
238+ for ( let i = 0 ; i < BULLET_COUNT ; i ++ ) {
239+ if ( bullets . next_free_or_spawned [ i ] != IS_SPAWNED )
240+ continue ;
241+ // debug: keep track of bullet count
242+ // bullet_count++;
243+
244+ const dir = bullets . directions [ i ] ;
245+ bullets . positions [ getActiveXyzIdx ( dir , i ) ] += dt * getSign ( dir ) * bullets . speeds [ i ] ;
246+ if ( isOutOfBounds ( bullets , i ) ) {
247+ freeBullet ( freelist , bullets , i ) ;
248+ continue ;
249+ }
250+ // debug: visualize bullet positions
251+ // DrawSphereEx(bullets.positions[i], CUBE_SIZE / 8.0f, 4, 4,
252+ // ColorFromNormalized(bullets.colors[i]));
253+
254+ const bbox = getBulletBoundingBox ( bullets , i ) ;
255+ const vIdx = getVec3Indices ( i ) ;
256+ for ( let z = bbox . min_z ; z < bbox . max_z ; z ++ ) {
257+ for ( let y = bbox . min_y ; y < bbox . max_y ; y ++ ) {
258+ for ( let x = bbox . min_x ; x < bbox . max_x ; x ++ ) {
259+ const cubeIdx = getVec3Indices ( CUBE_IDX ( x , y , z ) ) ;
260+ const dx = Math . abs ( ( bullets . positions [ vIdx . x ] - cubePositions [ cubeIdx . x ] ) / bullets . scales [ vIdx . x ] ) ;
261+ const dy = Math . abs ( ( bullets . positions [ vIdx . y ] - cubePositions [ cubeIdx . y ] ) / bullets . scales [ vIdx . y ] ) ;
262+ const dz = Math . abs ( ( bullets . positions [ vIdx . z ] - cubePositions [ cubeIdx . z ] ) / bullets . scales [ vIdx . z ] ) ;
263+ const d = dx + dy + dz ;
264+ const side_len = Math . max ( 0 , CUBE_SIZE * ( 1 - d ) ) ;
265+ if ( side_len <= EPSILON )
266+ continue ;
267+ // debug: bypass side length calc, show all cubes
268+ // const side_len = CUBE_SIZE;
269+ DrawCubeWires ( cubePosition , side_len , side_len , side_len ,
270+ ColorFromNormalized ( bullets . colors [ i ] ) ) ;
271+ }
272+ }
273+ }
274+ }
275+ gl . clear ( gl . COLOR_BUFFER_BIT | gl . DEPTH_BUFFER_BIT ) ;
276+
277+ requestAnimationFrame ( render ) ;
278+ } ;
279+
280+ requestAnimationFrame ( render ) ;
281+
282+ } ;
283+
284+ main ( ) ;
0 commit comments