pixospritz-core 0.10.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +36 -286
  2. package/dist/bundle.js +13 -3
  3. package/dist/bundle.js.map +1 -1
  4. package/dist/style.css +1 -0
  5. package/package.json +43 -44
  6. package/src/components/WebGLView.jsx +318 -0
  7. package/src/css/pixos.css +372 -0
  8. package/src/engine/actions/animate.js +41 -0
  9. package/src/engine/actions/changezone.js +135 -0
  10. package/src/engine/actions/chat.js +109 -0
  11. package/src/engine/actions/dialogue.js +90 -0
  12. package/src/engine/actions/face.js +22 -0
  13. package/src/engine/actions/greeting.js +28 -0
  14. package/src/engine/actions/interact.js +86 -0
  15. package/src/engine/actions/move.js +67 -0
  16. package/src/engine/actions/patrol.js +109 -0
  17. package/src/engine/actions/prompt.js +185 -0
  18. package/src/engine/actions/script.js +42 -0
  19. package/src/engine/core/audio/AudioSystem.js +543 -0
  20. package/src/engine/core/cutscene/PxcPlayer.js +956 -0
  21. package/src/engine/core/cutscene/manager.js +243 -0
  22. package/src/engine/core/database/index.js +75 -0
  23. package/src/engine/core/debug/index.js +371 -0
  24. package/src/engine/core/hud/index.js +765 -0
  25. package/src/engine/core/index.js +540 -0
  26. package/src/engine/core/input/gamepad/Controller.js +71 -0
  27. package/src/engine/core/input/gamepad/ControllerButtons.js +231 -0
  28. package/src/engine/core/input/gamepad/ControllerStick.js +173 -0
  29. package/src/engine/core/input/gamepad/index.js +592 -0
  30. package/src/engine/core/input/keyboard.js +196 -0
  31. package/src/engine/core/input/manager.js +485 -0
  32. package/src/engine/core/input/mouse.js +203 -0
  33. package/src/engine/core/input/touch.js +175 -0
  34. package/src/engine/core/mode/manager.js +199 -0
  35. package/src/engine/core/net/manager.js +535 -0
  36. package/src/engine/core/queue/action.js +83 -0
  37. package/src/engine/core/queue/event.js +82 -0
  38. package/src/engine/core/queue/index.js +44 -0
  39. package/src/engine/core/queue/loadable.js +33 -0
  40. package/src/engine/core/render/CameraEffects.js +494 -0
  41. package/src/engine/core/render/FrustumCuller.js +417 -0
  42. package/src/engine/core/render/LODManager.js +285 -0
  43. package/src/engine/core/render/ParticleManager.js +529 -0
  44. package/src/engine/core/render/TextureAtlas.js +465 -0
  45. package/src/engine/core/render/camera.js +338 -0
  46. package/src/engine/core/render/light.js +197 -0
  47. package/src/engine/core/render/manager.js +1079 -0
  48. package/src/engine/core/render/shaders.js +110 -0
  49. package/src/engine/core/render/skybox.js +342 -0
  50. package/src/engine/core/resource/manager.js +133 -0
  51. package/src/engine/core/resource/object.js +611 -0
  52. package/src/engine/core/resource/texture.js +103 -0
  53. package/src/engine/core/resource/tileset.js +177 -0
  54. package/src/engine/core/scene/avatar.js +215 -0
  55. package/src/engine/core/scene/speech.js +138 -0
  56. package/src/engine/core/scene/sprite.js +702 -0
  57. package/src/engine/core/scene/spritz.js +189 -0
  58. package/src/engine/core/scene/world.js +681 -0
  59. package/src/engine/core/scene/zone.js +1167 -0
  60. package/src/engine/core/store/index.js +110 -0
  61. package/src/engine/dynamic/animatedSprite.js +64 -0
  62. package/src/engine/dynamic/animatedTile.js +98 -0
  63. package/src/engine/dynamic/avatar.js +110 -0
  64. package/src/engine/dynamic/map.js +174 -0
  65. package/src/engine/dynamic/sprite.js +255 -0
  66. package/src/engine/dynamic/spritz.js +119 -0
  67. package/src/engine/events/EventSystem.js +609 -0
  68. package/src/engine/events/camera.js +142 -0
  69. package/src/engine/events/chat.js +75 -0
  70. package/src/engine/events/menu.js +186 -0
  71. package/src/engine/scripting/CallbackManager.js +514 -0
  72. package/src/engine/scripting/PixoScriptInterpreter.js +81 -0
  73. package/src/engine/scripting/PixoScriptLibrary.js +704 -0
  74. package/src/engine/shaders/effects/index.js +450 -0
  75. package/src/engine/shaders/fs.js +222 -0
  76. package/src/engine/shaders/particles/fs.js +41 -0
  77. package/src/engine/shaders/particles/vs.js +61 -0
  78. package/src/engine/shaders/picker/fs.js +34 -0
  79. package/src/engine/shaders/picker/init.js +62 -0
  80. package/src/engine/shaders/picker/vs.js +42 -0
  81. package/src/engine/shaders/pxsl/README.md +250 -0
  82. package/src/engine/shaders/pxsl/index.js +25 -0
  83. package/src/engine/shaders/pxsl/library.js +608 -0
  84. package/src/engine/shaders/pxsl/manager.js +338 -0
  85. package/src/engine/shaders/pxsl/specification.js +363 -0
  86. package/src/engine/shaders/pxsl/transpiler.js +753 -0
  87. package/src/engine/shaders/skybox/cosmic/fs.js +147 -0
  88. package/src/engine/shaders/skybox/cosmic/vs.js +23 -0
  89. package/src/engine/shaders/skybox/matrix/fs.js +127 -0
  90. package/src/engine/shaders/skybox/matrix/vs.js +23 -0
  91. package/src/engine/shaders/skybox/morning/fs.js +109 -0
  92. package/src/engine/shaders/skybox/morning/vs.js +23 -0
  93. package/src/engine/shaders/skybox/neon/fs.js +119 -0
  94. package/src/engine/shaders/skybox/neon/vs.js +23 -0
  95. package/src/engine/shaders/skybox/sky/fs.js +114 -0
  96. package/src/engine/shaders/skybox/sky/vs.js +23 -0
  97. package/src/engine/shaders/skybox/sunset/fs.js +101 -0
  98. package/src/engine/shaders/skybox/sunset/vs.js +23 -0
  99. package/src/engine/shaders/transition/blur/fs.js +42 -0
  100. package/src/engine/shaders/transition/blur/vs.js +26 -0
  101. package/src/engine/shaders/transition/cross/fs.js +36 -0
  102. package/src/engine/shaders/transition/cross/vs.js +26 -0
  103. package/src/engine/shaders/transition/crossBlur/fs.js +41 -0
  104. package/src/engine/shaders/transition/crossBlur/vs.js +25 -0
  105. package/src/engine/shaders/transition/dissolve/fs.js +78 -0
  106. package/src/engine/shaders/transition/dissolve/vs.js +24 -0
  107. package/src/engine/shaders/transition/fade/fs.js +31 -0
  108. package/src/engine/shaders/transition/fade/vs.js +27 -0
  109. package/src/engine/shaders/transition/iris/fs.js +52 -0
  110. package/src/engine/shaders/transition/iris/vs.js +24 -0
  111. package/src/engine/shaders/transition/pixelate/fs.js +44 -0
  112. package/src/engine/shaders/transition/pixelate/vs.js +24 -0
  113. package/src/engine/shaders/transition/slide/fs.js +53 -0
  114. package/src/engine/shaders/transition/slide/vs.js +24 -0
  115. package/src/engine/shaders/transition/swirl/fs.js +39 -0
  116. package/src/engine/shaders/transition/swirl/vs.js +26 -0
  117. package/src/engine/shaders/transition/wipe/fs.js +50 -0
  118. package/src/engine/shaders/transition/wipe/vs.js +24 -0
  119. package/src/engine/shaders/vs.js +60 -0
  120. package/src/engine/utils/CameraController.js +506 -0
  121. package/src/engine/utils/ObjHelper.js +551 -0
  122. package/src/engine/utils/debug-logger.js +110 -0
  123. package/src/engine/utils/enums.js +305 -0
  124. package/src/engine/utils/generator.js +156 -0
  125. package/src/engine/utils/index.js +21 -0
  126. package/src/engine/utils/loaders/ActionLoader.js +77 -0
  127. package/src/engine/utils/loaders/AudioLoader.js +157 -0
  128. package/src/engine/utils/loaders/EventLoader.js +66 -0
  129. package/src/engine/utils/loaders/ObjectLoader.js +67 -0
  130. package/src/engine/utils/loaders/SpriteLoader.js +77 -0
  131. package/src/engine/utils/loaders/TilesetLoader.js +103 -0
  132. package/src/engine/utils/loaders/index.js +21 -0
  133. package/src/engine/utils/math/matrix4.js +367 -0
  134. package/src/engine/utils/math/vector.js +458 -0
  135. package/src/engine/utils/obj/_old_js/index.js +46 -0
  136. package/src/engine/utils/obj/_old_js/layout.js +308 -0
  137. package/src/engine/utils/obj/_old_js/material.js +711 -0
  138. package/src/engine/utils/obj/_old_js/mesh.js +761 -0
  139. package/src/engine/utils/obj/_old_js/utils.js +647 -0
  140. package/src/engine/utils/obj/index.js +24 -0
  141. package/src/engine/utils/obj/js/index.js +277 -0
  142. package/src/engine/utils/obj/js/loader.js +232 -0
  143. package/src/engine/utils/obj/layout.js +246 -0
  144. package/src/engine/utils/obj/material.js +665 -0
  145. package/src/engine/utils/obj/mesh.js +657 -0
  146. package/src/engine/utils/obj/ts/index.ts +72 -0
  147. package/src/engine/utils/obj/ts/layout.ts +265 -0
  148. package/src/engine/utils/obj/ts/material.ts +760 -0
  149. package/src/engine/utils/obj/ts/mesh.ts +785 -0
  150. package/src/engine/utils/obj/ts/utils.ts +501 -0
  151. package/src/engine/utils/obj/utils.js +428 -0
  152. package/src/engine/utils/resources.js +18 -0
  153. package/src/index.jsx +55 -0
  154. package/src/spritz/player.js +18 -0
  155. package/src/spritz/readme.md +18 -0
  156. package/LICENSE +0 -437
  157. package/dist/bundle.js.LICENSE.txt +0 -31
@@ -0,0 +1,529 @@
1
+ /* *\
2
+ ** ----------------------------------------------- **
3
+ ** Calliope - Pixos Game Engine **
4
+ ** ----------------------------------------------- **
5
+ ** Copyright (c) 2020-2025 - Kyle Derby MacInnis **
6
+ ** **
7
+ ** Any unauthorized distribution or transfer **
8
+ ** of this work is strictly prohibited. **
9
+ ** **
10
+ ** All Rights Reserved. **
11
+ ** ----------------------------------------------- **
12
+ \* */
13
+
14
+ import { Vector } from '../../utils/math/vector.js';
15
+
16
+ /**
17
+ * @typedef {object} ParticleConfig
18
+ * @property {number} [count=8] - Number of particles to emit.
19
+ * @property {number} [life=1000] - Lifetime in milliseconds.
20
+ * @property {number} [speed=0.02] - Initial speed.
21
+ * @property {number} [spread=0.5] - Spread factor for direction.
22
+ * @property {number} [size=0.5] - Particle size.
23
+ * @property {number[]} [color=[1.0, 0.7, 0.2]] - RGB color array.
24
+ * @property {number[]} [gravity=[0, -0.00098, 0]] - Gravity vector.
25
+ * @property {number} [drag=0.995] - Drag coefficient.
26
+ * @property {string} [preset] - Preset name for quick config.
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} Particle
31
+ * @property {number[]} pos - Position [x, y, z].
32
+ * @property {number[]} vel - Velocity [vx, vy, vz].
33
+ * @property {number} life - Total lifetime.
34
+ * @property {number} age - Current age.
35
+ * @property {number} size - Size scalar.
36
+ * @property {number[]} color - RGB color.
37
+ * @property {number[]} gravity - Gravity vector.
38
+ * @property {number} drag - Drag coefficient.
39
+ */
40
+
41
+ /**
42
+ * ParticleManager - Manages particle effects in the Pixos game engine.
43
+ * Handles emission, physics updates, and rendering of particles.
44
+ */
45
+ export default class ParticleManager {
46
+ /**
47
+ * Creates an instance of ParticleManager.
48
+ * @param {import('./manager.js').default} renderManager - The render manager instance.
49
+ */
50
+ constructor(renderManager) {
51
+ /** @type {import('./manager.js').default} */
52
+ this.renderManager = renderManager;
53
+ /** @type {import('../index.js').default} */
54
+ this.engine = renderManager.engine;
55
+ /** @type {Particle[]} */
56
+ this.particles = [];
57
+ /** @type {boolean} */
58
+ this.initialized = false;
59
+ /** @type {WebGLBuffer|null} */
60
+ this.vertexPosBuf = null;
61
+ /** @type {WebGLBuffer|null} */
62
+ this.vertexTexBuf = null;
63
+ /** @type {number|null} */
64
+ this.lastUpdateTime = null;
65
+
66
+ // Instanced rendering buffers
67
+ /** @type {WebGLBuffer|null} */
68
+ this.instancePositionBuf = null;
69
+ /** @type {WebGLBuffer|null} */
70
+ this.instanceColorBuf = null;
71
+ /** @type {WebGLBuffer|null} */
72
+ this.instanceSizeBuf = null;
73
+ /** @type {boolean} */
74
+ this.useInstancing = true;
75
+ /** @type {number} */
76
+ this.maxInstances = 10000;
77
+ /** @type {Float32Array|null} */
78
+ this.instancePositions = null;
79
+ /** @type {Float32Array|null} */
80
+ this.instanceColors = null;
81
+ /** @type {Float32Array|null} */
82
+ this.instanceSizes = null;
83
+ }
84
+
85
+ /**
86
+ * Initializes GL buffers. Called after RenderManager has initialized shaders/GL.
87
+ * @returns {void}
88
+ */
89
+ init = () => {
90
+ /** @type {WebGL2RenderingContext} */
91
+ const gl = this.engine.gl;
92
+ if (!gl) return;
93
+
94
+ // A simple unit quad centered at origin (two triangles)
95
+ const quad = [
96
+ -0.5, -0.5, 0,
97
+ -0.5, 0.5, 0,
98
+ 0.5, 0.5, 0,
99
+ -0.5, -0.5, 0,
100
+ 0.5, 0.5, 0,
101
+ 0.5, -0.5, 0,
102
+ ];
103
+ // Simple UVs (not used when not texturing)
104
+ const uvs = [
105
+ 0, 0,
106
+ 0, 1,
107
+ 1, 1,
108
+ 0, 0,
109
+ 1, 1,
110
+ 1, 0,
111
+ ];
112
+
113
+ this.vertexPosBuf = this.renderManager.createBuffer(quad, gl.STATIC_DRAW, 3);
114
+ this.vertexTexBuf = this.renderManager.createBuffer(uvs, gl.STATIC_DRAW, 2);
115
+
116
+ // Initialize instanced rendering buffers
117
+ this.initInstancedBuffers();
118
+
119
+ this.initialized = true;
120
+ };
121
+
122
+ /**
123
+ * Initializes buffers for instanced rendering.
124
+ * @returns {void}
125
+ */
126
+ initInstancedBuffers = () => {
127
+ const gl = this.engine.gl;
128
+ if (!gl) return;
129
+
130
+ // Pre-allocate typed arrays for instance data
131
+ this.instancePositions = new Float32Array(this.maxInstances * 3);
132
+ this.instanceColors = new Float32Array(this.maxInstances * 4);
133
+ this.instanceSizes = new Float32Array(this.maxInstances);
134
+
135
+ // Create instance buffers
136
+ this.instancePositionBuf = gl.createBuffer();
137
+ this.instanceColorBuf = gl.createBuffer();
138
+ this.instanceSizeBuf = gl.createBuffer();
139
+ };
140
+
141
+ /**
142
+ * Updates instance buffers with current particle data.
143
+ * @returns {number} Number of particles to render.
144
+ */
145
+ updateInstanceBuffers = () => {
146
+ const gl = this.engine.gl;
147
+ if (!gl || !this.particles.length) return 0;
148
+
149
+ const count = Math.min(this.particles.length, this.maxInstances);
150
+
151
+ // Sort particles back-to-front for proper alpha blending
152
+ const cameraPos = this.renderManager.camera.cameraPosition;
153
+ const sortedParticles = [...this.particles].sort((a, b) => {
154
+ const distA = Math.pow(a.pos[0] - cameraPos.x, 2) +
155
+ Math.pow(a.pos[1] - cameraPos.y, 2) +
156
+ Math.pow(a.pos[2] - cameraPos.z, 2);
157
+ const distB = Math.pow(b.pos[0] - cameraPos.x, 2) +
158
+ Math.pow(b.pos[1] - cameraPos.y, 2) +
159
+ Math.pow(b.pos[2] - cameraPos.z, 2);
160
+ return distB - distA;
161
+ });
162
+
163
+ // Fill instance arrays
164
+ for (let i = 0; i < count; i++) {
165
+ const p = sortedParticles[i];
166
+ const lifeRatio = p.age / p.life;
167
+ const alpha = Math.max(0, 1.0 - lifeRatio * lifeRatio);
168
+
169
+ // Position (x, y, z)
170
+ this.instancePositions[i * 3] = p.pos[0];
171
+ this.instancePositions[i * 3 + 1] = p.pos[1];
172
+ this.instancePositions[i * 3 + 2] = p.pos[2];
173
+
174
+ // Color (r, g, b, a)
175
+ this.instanceColors[i * 4] = p.color[0];
176
+ this.instanceColors[i * 4 + 1] = p.color[1];
177
+ this.instanceColors[i * 4 + 2] = p.color[2];
178
+ this.instanceColors[i * 4 + 3] = alpha;
179
+
180
+ // Size
181
+ this.instanceSizes[i] = p.size;
182
+ }
183
+
184
+ // Upload to GPU
185
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instancePositionBuf);
186
+ gl.bufferData(gl.ARRAY_BUFFER, this.instancePositions.subarray(0, count * 3), gl.DYNAMIC_DRAW);
187
+
188
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceColorBuf);
189
+ gl.bufferData(gl.ARRAY_BUFFER, this.instanceColors.subarray(0, count * 4), gl.DYNAMIC_DRAW);
190
+
191
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceSizeBuf);
192
+ gl.bufferData(gl.ARRAY_BUFFER, this.instanceSizes.subarray(0, count), gl.DYNAMIC_DRAW);
193
+
194
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
195
+
196
+ return count;
197
+ };
198
+
199
+ /**
200
+ * Emits particles based on a config object.
201
+ * @param {number[]|Vector} [position=[0, 0, 0]] - Position [x, y, z] or Vector.
202
+ * @param {ParticleConfig} [config={}] - Configuration for particles.
203
+ * @returns {void}
204
+ */
205
+ emit = (position = [0, 0, 0], config = {}) => {
206
+ /** @type {number[]} */
207
+ let pos = Array.isArray(position) ? position : position.toArray ? position.toArray() : [0, 0, 0];
208
+ let x = pos[0], y = pos[1], zOffset = pos[2] || 0;
209
+ /** @type {import('../../scene/zone.js').Zone|null} */
210
+ let zone = this.engine.spritz.world.zoneContaining(x, y);
211
+ let z = zOffset;
212
+ if (zone) {
213
+ z += zone.getHeight(x, y);
214
+ }
215
+
216
+ pos = [x, y, z];
217
+
218
+ /** @type {ParticleConfig} */
219
+ const c = Object.assign(
220
+ {
221
+ count: 8,
222
+ life: 1000, // ms
223
+ speed: 0.02,
224
+ spread: 0.5,
225
+ size: 0.5,
226
+ color: [1.0, 0.7, 0.2],
227
+ gravity: [0, -0.00098, 0],
228
+ drag: 0.995,
229
+ },
230
+ config
231
+ );
232
+
233
+ for (let i = 0; i < c.count; i++) {
234
+ // random direction in unit sphere
235
+ const rx = (Math.random() * 2 - 1) * c.spread;
236
+ const ry = (Math.random() * 2 - 1) * c.spread;
237
+ const rz = (Math.random() * 2 - 1) * c.spread;
238
+ const vx = rx * c.speed * (0.5 + Math.random() * 1.5);
239
+ const vy = ry * c.speed * (0.5 + Math.random() * 1.5);
240
+ const vz = rz * c.speed * (0.5 + Math.random() * 1.5);
241
+
242
+ /** @type {Particle} */
243
+ const particle = {
244
+ pos: [pos[0], pos[1], pos[2]],
245
+ vel: [vx, vy, vz],
246
+ life: c.life,
247
+ age: 0,
248
+ size: c.size * (0.8 + Math.random() * 0.8),
249
+ color: c.color,
250
+ gravity: c.gravity,
251
+ drag: c.drag,
252
+ };
253
+ this.particles.push(particle);
254
+ }
255
+ };
256
+
257
+ /**
258
+ * Returns a preset configuration for particles.
259
+ * @param {string} name - The preset name.
260
+ * @returns {ParticleConfig|null} The preset config or null if not found.
261
+ */
262
+ preset = (name) => {
263
+ switch ((name || '').toLowerCase()) {
264
+ case 'sparks':
265
+ return { count: 12, life: 700, speed: 0.06, spread: 1.2, size: 0.15, color: [1, 0.8, 0.2], gravity: [0, -0.002, 0] };
266
+ case 'flame':
267
+ return { count: 200, life: 2000, speed: 0.02, spread: 0.8, size: 0.06, color: [1, 0.5, 0.1], gravity: [0, -0.0003, 0], drag: 0.995 };
268
+ case 'water':
269
+ return { count: 20, life: 800, speed: 0.05, spread: 1.5, size: 0.12, color: [0.6, 0.7, 1.0], gravity: [0, -0.003, 0], drag: 0.996 };
270
+ case 'weapon':
271
+ return { count: 6, life: 600, speed: 0.08, spread: 0.3, size: 0.18, color: [1, 1, 0.6], gravity: [0, -0.001, 0] };
272
+ default:
273
+ return null;
274
+ }
275
+ };
276
+
277
+ /**
278
+ * Updates particle physics. Timestamp in ms.
279
+ * @param {number} timestamp - Current timestamp.
280
+ * @returns {void}
281
+ */
282
+ update = (timestamp) => {
283
+ if (!this.lastUpdateTime) this.lastUpdateTime = timestamp;
284
+ const dt = timestamp - this.lastUpdateTime;
285
+ this.lastUpdateTime = timestamp;
286
+ if (!dt) return;
287
+
288
+ // Update particle physics
289
+ for (let i = this.particles.length - 1; i >= 0; i--) {
290
+ /** @type {Particle} */
291
+ const p = this.particles[i];
292
+ // apply gravity
293
+ p.vel[0] += (p.gravity[0] || 0) * dt;
294
+ p.vel[1] += (p.gravity[1] || 0) * dt;
295
+ p.vel[2] += (p.gravity[2] || 0) * dt;
296
+ // apply drag
297
+ p.vel[0] *= Math.pow(p.drag || 1, dt / 16.6667);
298
+ p.vel[1] *= Math.pow(p.drag || 1, dt / 16.6667);
299
+ p.vel[2] *= Math.pow(p.drag || 1, dt / 16.6667);
300
+ // integrate
301
+ p.pos[0] += p.vel[0] * dt;
302
+ p.pos[1] += p.vel[1] * dt;
303
+ p.pos[2] += p.vel[2] * dt;
304
+ p.age += dt;
305
+ if (p.age >= p.life) {
306
+ this.particles.splice(i, 1);
307
+ }
308
+ }
309
+ };
310
+
311
+ /**
312
+ * Renders particles using instanced rendering for optimal performance.
313
+ * Falls back to individual draw calls if instancing is not available.
314
+ * @returns {void}
315
+ */
316
+ render = () => {
317
+ if (!this.initialized) this.init();
318
+ if (!this.initialized) return;
319
+ if (!this.particles.length) return;
320
+
321
+ /** @type {import('./manager.js').default} */
322
+ const rm = this.renderManager;
323
+ /** @type {WebGL2RenderingContext} */
324
+ const gl = this.engine.gl;
325
+ /** @type {WebGLProgram} */
326
+ const shader = rm.particleShaderProgram;
327
+ if (!shader) return;
328
+
329
+ // Check for instanced rendering support (WebGL2)
330
+ const supportsInstancing = typeof gl.drawArraysInstanced === 'function';
331
+
332
+ if (supportsInstancing && this.useInstancing && shader.aInstancePosition !== undefined) {
333
+ this.renderInstanced();
334
+ } else {
335
+ this.renderNonInstanced();
336
+ }
337
+ };
338
+
339
+ /**
340
+ * Renders particles using WebGL2 instanced rendering.
341
+ * Single draw call for all particles - massive performance improvement.
342
+ * @returns {void}
343
+ */
344
+ renderInstanced = () => {
345
+ const rm = this.renderManager;
346
+ const gl = this.engine.gl;
347
+ const shader = rm.particleShaderProgram;
348
+
349
+ // Update instance buffers with current particle data
350
+ const instanceCount = this.updateInstanceBuffers();
351
+ if (instanceCount === 0) return;
352
+
353
+ // Reset vertex attrib arrays
354
+ for (let i = 0; i < 8; i++) {
355
+ gl.disableVertexAttribArray(i);
356
+ }
357
+
358
+ gl.useProgram(shader);
359
+
360
+ // Enable blending for transparency - additive blending for glow
361
+ gl.enable(gl.BLEND);
362
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
363
+ gl.depthMask(false);
364
+
365
+ // Bind quad vertex data (shared for all instances)
366
+ gl.enableVertexAttribArray(shader.aVertexPosition);
367
+ rm.bindBuffer(this.vertexPosBuf, shader.aVertexPosition);
368
+
369
+ gl.enableVertexAttribArray(shader.aTextureCoord);
370
+ rm.bindBuffer(this.vertexTexBuf, shader.aTextureCoord);
371
+
372
+ // Bind instance position buffer
373
+ if (shader.aInstancePosition !== undefined) {
374
+ gl.enableVertexAttribArray(shader.aInstancePosition);
375
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instancePositionBuf);
376
+ gl.vertexAttribPointer(shader.aInstancePosition, 3, gl.FLOAT, false, 0, 0);
377
+ gl.vertexAttribDivisor(shader.aInstancePosition, 1);
378
+ }
379
+
380
+ // Bind instance color buffer
381
+ if (shader.aInstanceColor !== undefined) {
382
+ gl.enableVertexAttribArray(shader.aInstanceColor);
383
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceColorBuf);
384
+ gl.vertexAttribPointer(shader.aInstanceColor, 4, gl.FLOAT, false, 0, 0);
385
+ gl.vertexAttribDivisor(shader.aInstanceColor, 1);
386
+ }
387
+
388
+ // Bind instance size buffer
389
+ if (shader.aInstanceSize !== undefined) {
390
+ gl.enableVertexAttribArray(shader.aInstanceSize);
391
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceSizeBuf);
392
+ gl.vertexAttribPointer(shader.aInstanceSize, 1, gl.FLOAT, false, 0, 0);
393
+ gl.vertexAttribDivisor(shader.aInstanceSize, 1);
394
+ }
395
+
396
+ // Set projection/view matrices
397
+ if (shader.setMatrixUniforms) {
398
+ shader.setMatrixUniforms({});
399
+ }
400
+
401
+ // Draw all particles in a single instanced call
402
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
403
+
404
+ // Reset divisors
405
+ if (shader.aInstancePosition !== undefined) {
406
+ gl.vertexAttribDivisor(shader.aInstancePosition, 0);
407
+ gl.disableVertexAttribArray(shader.aInstancePosition);
408
+ }
409
+ if (shader.aInstanceColor !== undefined) {
410
+ gl.vertexAttribDivisor(shader.aInstanceColor, 0);
411
+ gl.disableVertexAttribArray(shader.aInstanceColor);
412
+ }
413
+ if (shader.aInstanceSize !== undefined) {
414
+ gl.vertexAttribDivisor(shader.aInstanceSize, 0);
415
+ gl.disableVertexAttribArray(shader.aInstanceSize);
416
+ }
417
+
418
+ // Cleanup
419
+ gl.depthMask(true);
420
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
421
+ gl.disableVertexAttribArray(shader.aVertexPosition);
422
+ gl.disableVertexAttribArray(shader.aTextureCoord);
423
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
424
+ gl.useProgram(null);
425
+ };
426
+
427
+ /**
428
+ * Renders particles without instancing (fallback for older systems).
429
+ * @returns {void}
430
+ */
431
+ renderNonInstanced = () => {
432
+ const rm = this.renderManager;
433
+ const gl = this.engine.gl;
434
+ const shader = rm.particleShaderProgram;
435
+
436
+ // Reset all vertex attrib arrays to prevent errors from other shaders
437
+ for (let i = 0; i < 8; i++) {
438
+ gl.disableVertexAttribArray(i);
439
+ }
440
+
441
+ // Use particle shader
442
+ gl.useProgram(shader);
443
+
444
+ // Enable blending for transparency - additive blending for glow effects
445
+ gl.enable(gl.BLEND);
446
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
447
+
448
+ // Disable depth writing (but keep depth test) for proper transparency
449
+ gl.depthMask(false);
450
+
451
+ // Enable vertex attributes for particle shader
452
+ gl.enableVertexAttribArray(shader.aVertexPosition);
453
+ gl.enableVertexAttribArray(shader.aTextureCoord);
454
+
455
+ // Sort particles back-to-front based on distance from camera
456
+ const cameraPos = rm.camera.cameraPosition;
457
+ const sortedParticles = [...this.particles].sort((a, b) => {
458
+ const distA = Math.pow(a.pos[0] - cameraPos.x, 2) +
459
+ Math.pow(a.pos[1] - cameraPos.y, 2) +
460
+ Math.pow(a.pos[2] - cameraPos.z, 2);
461
+ const distB = Math.pow(b.pos[0] - cameraPos.x, 2) +
462
+ Math.pow(b.pos[1] - cameraPos.y, 2) +
463
+ Math.pow(b.pos[2] - cameraPos.z, 2);
464
+ return distB - distA; // Back to front
465
+ });
466
+
467
+ for (const p of sortedParticles) {
468
+ rm.mvPushMatrix();
469
+
470
+ // Set model matrix translation only (billboarding handled in shader)
471
+ const m = rm.uModelMat;
472
+ // Reset to identity
473
+ for (let i = 0; i < 16; i++) m[i] = (i % 5 === 0) ? 1 : 0;
474
+ // Set translation
475
+ m[12] = p.pos[0];
476
+ m[13] = p.pos[1];
477
+ m[14] = p.pos[2];
478
+
479
+ // Calculate alpha based on particle age (fade out towards end of life)
480
+ const lifeRatio = p.age / p.life;
481
+ const alpha = Math.max(0, 1.0 - lifeRatio * lifeRatio); // Quadratic fade
482
+
483
+ // Set scale and matrix uniforms with alpha
484
+ const scaleVec = new Vector(p.size, p.size, p.size);
485
+ shader.setMatrixUniforms({ scale: scaleVec, color: p.color, alpha: alpha });
486
+
487
+ // Bind buffers and draw
488
+ rm.bindBuffer(this.vertexPosBuf, shader.aVertexPosition);
489
+ rm.bindBuffer(this.vertexTexBuf, shader.aTextureCoord);
490
+ gl.drawArrays(gl.TRIANGLES, 0, this.vertexPosBuf.numItems);
491
+
492
+ rm.mvPopMatrix();
493
+ }
494
+
495
+ // Restore depth mask and blending
496
+ gl.depthMask(true);
497
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
498
+
499
+ // Disable vertex attrib arrays to prevent WebGL state issues
500
+ gl.disableVertexAttribArray(shader.aVertexPosition);
501
+ gl.disableVertexAttribArray(shader.aTextureCoord);
502
+
503
+ // cleanup
504
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
505
+ gl.useProgram(null);
506
+ };
507
+
508
+ /**
509
+ * Gets particle count for debugging/stats.
510
+ * @returns {number} Current particle count.
511
+ */
512
+ getParticleCount = () => this.particles.length;
513
+
514
+ /**
515
+ * Clears all particles.
516
+ * @returns {void}
517
+ */
518
+ clear = () => {
519
+ this.particles = [];
520
+ };
521
+
522
+ /**
523
+ * Sets whether to use instanced rendering.
524
+ * @param {boolean} enabled - Whether to use instancing.
525
+ */
526
+ setInstancingEnabled = (enabled) => {
527
+ this.useInstancing = enabled;
528
+ };
529
+ }