bloody-engine 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/node/batch-renderer-JqZ4TYcL.js +308 -0
- package/dist/node/browser-resource-loader-D51BD3k_.js +146 -0
- package/dist/node/camera-A8EGrk7U.js +271 -0
- package/dist/node/index-node.js +2117 -0
- package/dist/node/node-resource-loader-MzkD-IGo.js +166 -0
- package/dist/node/resource-loader-factory-DQ-PAVcN.js +93 -0
- package/dist/node/resource-pipeline-Dac9qRso.js +211 -0
- package/dist/web/index.js +1940 -0
- package/dist/web/index.umd.js +56 -0
- package/package.json +58 -0
|
@@ -0,0 +1,2117 @@
|
|
|
1
|
+
import sdl from "@kmamal/sdl";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import path__default from "path";
|
|
5
|
+
import createGL from "gl";
|
|
6
|
+
class Texture {
|
|
7
|
+
/**
|
|
8
|
+
* Create a texture from pixel data
|
|
9
|
+
* @param gl WebGL context
|
|
10
|
+
* @param width Texture width
|
|
11
|
+
* @param height Texture height
|
|
12
|
+
* @param data Pixel data (Uint8Array RGBA)
|
|
13
|
+
*/
|
|
14
|
+
constructor(gl2, width, height, data) {
|
|
15
|
+
this.gl = gl2;
|
|
16
|
+
this.width = width;
|
|
17
|
+
this.height = height;
|
|
18
|
+
const tex = gl2.createTexture();
|
|
19
|
+
if (!tex) {
|
|
20
|
+
throw new Error("Failed to create texture");
|
|
21
|
+
}
|
|
22
|
+
this.texture = tex;
|
|
23
|
+
gl2.bindTexture(gl2.TEXTURE_2D, this.texture);
|
|
24
|
+
gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_S, gl2.CLAMP_TO_EDGE);
|
|
25
|
+
gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_T, gl2.CLAMP_TO_EDGE);
|
|
26
|
+
gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MIN_FILTER, gl2.LINEAR);
|
|
27
|
+
gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MAG_FILTER, gl2.LINEAR);
|
|
28
|
+
if (data) {
|
|
29
|
+
gl2.texImage2D(
|
|
30
|
+
gl2.TEXTURE_2D,
|
|
31
|
+
0,
|
|
32
|
+
gl2.RGBA,
|
|
33
|
+
width,
|
|
34
|
+
height,
|
|
35
|
+
0,
|
|
36
|
+
gl2.RGBA,
|
|
37
|
+
gl2.UNSIGNED_BYTE,
|
|
38
|
+
data
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
gl2.texImage2D(
|
|
42
|
+
gl2.TEXTURE_2D,
|
|
43
|
+
0,
|
|
44
|
+
gl2.RGBA,
|
|
45
|
+
width,
|
|
46
|
+
height,
|
|
47
|
+
0,
|
|
48
|
+
gl2.RGBA,
|
|
49
|
+
gl2.UNSIGNED_BYTE,
|
|
50
|
+
null
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
gl2.bindTexture(gl2.TEXTURE_2D, null);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create a solid color texture
|
|
57
|
+
* @param gl WebGL context
|
|
58
|
+
* @param width Texture width
|
|
59
|
+
* @param height Texture height
|
|
60
|
+
* @param r Red (0-255)
|
|
61
|
+
* @param g Green (0-255)
|
|
62
|
+
* @param b Blue (0-255)
|
|
63
|
+
* @param a Alpha (0-255)
|
|
64
|
+
*/
|
|
65
|
+
static createSolid(gl2, width, height, r, g, b, a = 255) {
|
|
66
|
+
const pixelCount = width * height;
|
|
67
|
+
const data = new Uint8Array(pixelCount * 4);
|
|
68
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
69
|
+
const offset = i * 4;
|
|
70
|
+
data[offset] = r;
|
|
71
|
+
data[offset + 1] = g;
|
|
72
|
+
data[offset + 2] = b;
|
|
73
|
+
data[offset + 3] = a;
|
|
74
|
+
}
|
|
75
|
+
return new Texture(gl2, width, height, data);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create a checkerboard texture
|
|
79
|
+
* @param gl WebGL context
|
|
80
|
+
* @param width Texture width
|
|
81
|
+
* @param height Texture height
|
|
82
|
+
* @param squareSize Size of each square
|
|
83
|
+
*/
|
|
84
|
+
static createCheckerboard(gl2, width, height, squareSize = 32) {
|
|
85
|
+
const data = new Uint8Array(width * height * 4);
|
|
86
|
+
for (let y = 0; y < height; y++) {
|
|
87
|
+
for (let x = 0; x < width; x++) {
|
|
88
|
+
const squareX = Math.floor(x / squareSize);
|
|
89
|
+
const squareY = Math.floor(y / squareSize);
|
|
90
|
+
const isWhite = (squareX + squareY) % 2 === 0;
|
|
91
|
+
const offset = (y * width + x) * 4;
|
|
92
|
+
const color = isWhite ? 255 : 0;
|
|
93
|
+
data[offset] = color;
|
|
94
|
+
data[offset + 1] = color;
|
|
95
|
+
data[offset + 2] = color;
|
|
96
|
+
data[offset + 3] = 255;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return new Texture(gl2, width, height, data);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Create a gradient texture
|
|
103
|
+
* @param gl WebGL context
|
|
104
|
+
* @param width Texture width
|
|
105
|
+
* @param height Texture height
|
|
106
|
+
*/
|
|
107
|
+
static createGradient(gl2, width, height) {
|
|
108
|
+
const data = new Uint8Array(width * height * 4);
|
|
109
|
+
for (let y = 0; y < height; y++) {
|
|
110
|
+
for (let x = 0; x < width; x++) {
|
|
111
|
+
const offset = (y * width + x) * 4;
|
|
112
|
+
data[offset] = Math.floor(x / width * 255);
|
|
113
|
+
data[offset + 1] = Math.floor(y / height * 255);
|
|
114
|
+
data[offset + 2] = 128;
|
|
115
|
+
data[offset + 3] = 255;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return new Texture(gl2, width, height, data);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Bind this texture to a texture unit
|
|
122
|
+
* @param unit Texture unit (0-7 typically)
|
|
123
|
+
*/
|
|
124
|
+
bind(unit = 0) {
|
|
125
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + unit);
|
|
126
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Unbind texture
|
|
130
|
+
*/
|
|
131
|
+
unbind() {
|
|
132
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, null);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get the underlying WebGL texture
|
|
136
|
+
*/
|
|
137
|
+
getHandle() {
|
|
138
|
+
return this.texture;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get texture dimensions
|
|
142
|
+
*/
|
|
143
|
+
getDimensions() {
|
|
144
|
+
return { width: this.width, height: this.height };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Clean up texture resources
|
|
148
|
+
*/
|
|
149
|
+
dispose() {
|
|
150
|
+
this.gl.deleteTexture(this.texture);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const texture = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
154
|
+
__proto__: null,
|
|
155
|
+
Texture
|
|
156
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
157
|
+
class VertexBuffer {
|
|
158
|
+
constructor(gl2, data, stride = 0) {
|
|
159
|
+
this.gl = gl2;
|
|
160
|
+
this.stride = stride;
|
|
161
|
+
const floatsPerVertex = stride > 0 ? stride / 4 : 3;
|
|
162
|
+
this.vertexCount = data.length / floatsPerVertex;
|
|
163
|
+
const buf = gl2.createBuffer();
|
|
164
|
+
if (!buf) {
|
|
165
|
+
throw new Error("Failed to create vertex buffer");
|
|
166
|
+
}
|
|
167
|
+
this.buffer = buf;
|
|
168
|
+
gl2.bindBuffer(gl2.ARRAY_BUFFER, this.buffer);
|
|
169
|
+
gl2.bufferData(gl2.ARRAY_BUFFER, data, gl2.STATIC_DRAW);
|
|
170
|
+
gl2.bindBuffer(gl2.ARRAY_BUFFER, null);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Bind buffer for rendering
|
|
174
|
+
*/
|
|
175
|
+
bind() {
|
|
176
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Unbind buffer
|
|
180
|
+
*/
|
|
181
|
+
unbind() {
|
|
182
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get vertex count
|
|
186
|
+
*/
|
|
187
|
+
getVertexCount() {
|
|
188
|
+
return this.vertexCount;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get stride
|
|
192
|
+
*/
|
|
193
|
+
getStride() {
|
|
194
|
+
return this.stride;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Clean up resources
|
|
198
|
+
*/
|
|
199
|
+
dispose() {
|
|
200
|
+
this.gl.deleteBuffer(this.buffer);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const buffer = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
204
|
+
__proto__: null,
|
|
205
|
+
VertexBuffer
|
|
206
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
207
|
+
class SDLWindow {
|
|
208
|
+
constructor(width, height, title = "Bloody Engine") {
|
|
209
|
+
this.closed = false;
|
|
210
|
+
this.width = width;
|
|
211
|
+
this.height = height;
|
|
212
|
+
this.title = title;
|
|
213
|
+
try {
|
|
214
|
+
this.window = sdl.video.createWindow({
|
|
215
|
+
width: this.width,
|
|
216
|
+
height: this.height,
|
|
217
|
+
title: this.title
|
|
218
|
+
});
|
|
219
|
+
if (!this.window) {
|
|
220
|
+
throw new Error("Failed to create SDL window");
|
|
221
|
+
}
|
|
222
|
+
this.window.on("close", () => {
|
|
223
|
+
this.closed = true;
|
|
224
|
+
});
|
|
225
|
+
console.log(`✓ SDL Window created (${width}x${height}): "${title}"`);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.cleanup();
|
|
228
|
+
throw new Error(`Window creation failed: ${error}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get window dimensions
|
|
233
|
+
*/
|
|
234
|
+
getDimensions() {
|
|
235
|
+
return { width: this.width, height: this.height };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Display pixel data in the window
|
|
239
|
+
* @param pixels Uint8Array of RGBA pixel data
|
|
240
|
+
*/
|
|
241
|
+
updatePixels(pixels) {
|
|
242
|
+
if (!this.window || this.closed) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const buffer2 = Buffer.from(pixels);
|
|
247
|
+
const stride = this.width * 4;
|
|
248
|
+
this.window.render(this.width, this.height, stride, "rgba32", buffer2);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error("Failed to update pixels:", error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Register an event handler
|
|
255
|
+
*/
|
|
256
|
+
on(eventName, handler) {
|
|
257
|
+
if (!this.window || this.closed) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
this.window.on(eventName, (event) => {
|
|
262
|
+
try {
|
|
263
|
+
handler(event);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error(`Error in ${eventName} handler:`, error);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(`Error registering ${eventName} handler:`, error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Check if window is still open
|
|
274
|
+
*/
|
|
275
|
+
isOpen() {
|
|
276
|
+
return this.window !== null && !this.closed;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Cleanup and close window
|
|
280
|
+
*/
|
|
281
|
+
cleanup() {
|
|
282
|
+
if (this.window && !this.closed) {
|
|
283
|
+
try {
|
|
284
|
+
this.window.destroy();
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.warn("Error destroying window:", error);
|
|
287
|
+
}
|
|
288
|
+
this.window = null;
|
|
289
|
+
this.closed = true;
|
|
290
|
+
}
|
|
291
|
+
console.log("✓ SDL Window cleaned up");
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Destroy the window (alias for cleanup)
|
|
295
|
+
*/
|
|
296
|
+
destroy() {
|
|
297
|
+
this.cleanup();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
class ProjectionConfig {
|
|
301
|
+
// Scale factor for height (vertical exaggeration)
|
|
302
|
+
constructor(tileWidth = 64, tileHeight = 32, zScale = 1) {
|
|
303
|
+
this.tileWidth = tileWidth;
|
|
304
|
+
this.tileHeight = tileHeight;
|
|
305
|
+
this.zScale = zScale;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function gridToScreen(gridCoord, config) {
|
|
309
|
+
const halfTileWidth = config.tileWidth / 2;
|
|
310
|
+
const halfTileHeight = config.tileHeight / 2;
|
|
311
|
+
const xscreen = (gridCoord.xgrid - gridCoord.ygrid) * halfTileWidth;
|
|
312
|
+
const yscreen = (gridCoord.xgrid + gridCoord.ygrid) * halfTileHeight - gridCoord.zheight * config.zScale;
|
|
313
|
+
return { xscreen, yscreen };
|
|
314
|
+
}
|
|
315
|
+
function getCellCenterOffset(config) {
|
|
316
|
+
return {
|
|
317
|
+
xscreen: 0,
|
|
318
|
+
yscreen: config.tileHeight / 2
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function createDefaultProjection() {
|
|
322
|
+
return new ProjectionConfig(64, 32, 1);
|
|
323
|
+
}
|
|
324
|
+
const SCENE_CONFIG = {
|
|
325
|
+
width: 800,
|
|
326
|
+
height: 600,
|
|
327
|
+
targetFPS: 60
|
|
328
|
+
};
|
|
329
|
+
const PROJECTION_CONFIG = new ProjectionConfig(
|
|
330
|
+
64,
|
|
331
|
+
// tileWidth: 64 pixels
|
|
332
|
+
32,
|
|
333
|
+
// tileHeight: 32 pixels
|
|
334
|
+
1
|
|
335
|
+
// zScale: 1:1 height scale
|
|
336
|
+
);
|
|
337
|
+
const PROJECTION_ENTITIES = [
|
|
338
|
+
{
|
|
339
|
+
id: "player",
|
|
340
|
+
name: "Player",
|
|
341
|
+
gridPos: { xgrid: 5, ygrid: 5, zheight: 0 },
|
|
342
|
+
color: [0.2, 1, 0.2],
|
|
343
|
+
// Green
|
|
344
|
+
size: 1
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "enemy1",
|
|
348
|
+
name: "Enemy",
|
|
349
|
+
gridPos: { xgrid: 10, ygrid: 8, zheight: 0 },
|
|
350
|
+
color: [1, 0.2, 0.2],
|
|
351
|
+
// Red
|
|
352
|
+
size: 0.9
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: "chest",
|
|
356
|
+
name: "Treasure",
|
|
357
|
+
gridPos: { xgrid: 8, ygrid: 6, zheight: 0 },
|
|
358
|
+
color: [1, 1, 0.2],
|
|
359
|
+
// Yellow
|
|
360
|
+
size: 0.7
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: "tower",
|
|
364
|
+
name: "Tower",
|
|
365
|
+
gridPos: { xgrid: 12, ygrid: 12, zheight: 3 },
|
|
366
|
+
color: [0.5, 0.5, 1],
|
|
367
|
+
// Light Blue
|
|
368
|
+
size: 0.8
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
id: "floating",
|
|
372
|
+
name: "Floating Object",
|
|
373
|
+
gridPos: { xgrid: 3, ygrid: 10, zheight: 5 },
|
|
374
|
+
color: [1, 0.5, 1],
|
|
375
|
+
// Magenta
|
|
376
|
+
size: 0.6
|
|
377
|
+
}
|
|
378
|
+
];
|
|
379
|
+
const GEOMETRY = {
|
|
380
|
+
quad: {
|
|
381
|
+
name: "quad",
|
|
382
|
+
vertices: new Float32Array([
|
|
383
|
+
// Position TexCoord
|
|
384
|
+
-0.5,
|
|
385
|
+
-0.5,
|
|
386
|
+
0,
|
|
387
|
+
0,
|
|
388
|
+
0,
|
|
389
|
+
// Bottom-left
|
|
390
|
+
0.5,
|
|
391
|
+
-0.5,
|
|
392
|
+
0,
|
|
393
|
+
1,
|
|
394
|
+
0,
|
|
395
|
+
// Bottom-right
|
|
396
|
+
0.5,
|
|
397
|
+
0.5,
|
|
398
|
+
0,
|
|
399
|
+
1,
|
|
400
|
+
1,
|
|
401
|
+
// Top-right
|
|
402
|
+
0.5,
|
|
403
|
+
0.5,
|
|
404
|
+
0,
|
|
405
|
+
1,
|
|
406
|
+
1,
|
|
407
|
+
// Top-right
|
|
408
|
+
-0.5,
|
|
409
|
+
0.5,
|
|
410
|
+
0,
|
|
411
|
+
0,
|
|
412
|
+
1,
|
|
413
|
+
// Top-left
|
|
414
|
+
-0.5,
|
|
415
|
+
-0.5,
|
|
416
|
+
0,
|
|
417
|
+
0,
|
|
418
|
+
0
|
|
419
|
+
// Bottom-left
|
|
420
|
+
]),
|
|
421
|
+
stride: 5 * 4
|
|
422
|
+
// 5 floats per vertex × 4 bytes
|
|
423
|
+
},
|
|
424
|
+
triangle: {
|
|
425
|
+
name: "triangle",
|
|
426
|
+
vertices: new Float32Array([
|
|
427
|
+
// Position TexCoord
|
|
428
|
+
0,
|
|
429
|
+
0.5,
|
|
430
|
+
0,
|
|
431
|
+
0.5,
|
|
432
|
+
1,
|
|
433
|
+
// Top
|
|
434
|
+
-0.5,
|
|
435
|
+
-0.5,
|
|
436
|
+
0,
|
|
437
|
+
0,
|
|
438
|
+
0,
|
|
439
|
+
// Bottom-left
|
|
440
|
+
0.5,
|
|
441
|
+
-0.5,
|
|
442
|
+
0,
|
|
443
|
+
1,
|
|
444
|
+
0
|
|
445
|
+
// Bottom-right
|
|
446
|
+
]),
|
|
447
|
+
stride: 5 * 4
|
|
448
|
+
// 5 floats per vertex × 4 bytes
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const SHADERS_V2 = {
|
|
452
|
+
vertex: `
|
|
453
|
+
attribute vec3 aPosition;
|
|
454
|
+
attribute vec2 aTexCoord;
|
|
455
|
+
attribute vec4 aColor;
|
|
456
|
+
attribute float aTexIndex;
|
|
457
|
+
|
|
458
|
+
varying vec2 vTexCoord;
|
|
459
|
+
varying vec4 vColor;
|
|
460
|
+
varying float vTexIndex;
|
|
461
|
+
|
|
462
|
+
uniform mat4 uMatrix;
|
|
463
|
+
|
|
464
|
+
void main() {
|
|
465
|
+
gl_Position = uMatrix * vec4(aPosition, 1.0);
|
|
466
|
+
vTexCoord = aTexCoord;
|
|
467
|
+
vColor = aColor;
|
|
468
|
+
vTexIndex = aTexIndex;
|
|
469
|
+
}
|
|
470
|
+
`,
|
|
471
|
+
fragment: `
|
|
472
|
+
precision mediump float;
|
|
473
|
+
|
|
474
|
+
varying vec2 vTexCoord;
|
|
475
|
+
varying vec4 vColor;
|
|
476
|
+
varying float vTexIndex;
|
|
477
|
+
|
|
478
|
+
uniform sampler2D uTexture;
|
|
479
|
+
|
|
480
|
+
void main() {
|
|
481
|
+
// Sample texture
|
|
482
|
+
vec4 texColor = texture2D(uTexture, vTexCoord);
|
|
483
|
+
|
|
484
|
+
// Apply vertex color tint
|
|
485
|
+
gl_FragColor = texColor * vColor;
|
|
486
|
+
}
|
|
487
|
+
`
|
|
488
|
+
};
|
|
489
|
+
const TEXTURE_CONFIG = {
|
|
490
|
+
size: 256,
|
|
491
|
+
type: "gradient"
|
|
492
|
+
// gradient, solid, etc.
|
|
493
|
+
};
|
|
494
|
+
const ANIMATION_CONFIG = {
|
|
495
|
+
background: {
|
|
496
|
+
offsetR: 0.1,
|
|
497
|
+
amplitudeR: 0.1,
|
|
498
|
+
speedR: 1,
|
|
499
|
+
offsetG: 0.1,
|
|
500
|
+
amplitudeG: 0.1,
|
|
501
|
+
speedG: 0.7,
|
|
502
|
+
offsetB: 0.15,
|
|
503
|
+
amplitudeB: 0.1,
|
|
504
|
+
speedB: 0.5
|
|
505
|
+
},
|
|
506
|
+
quads: {
|
|
507
|
+
count: 3,
|
|
508
|
+
scale: 0.4,
|
|
509
|
+
baseRadius: 0.5,
|
|
510
|
+
radiusVariation: 0.2,
|
|
511
|
+
radiusSpeed: 0.3,
|
|
512
|
+
orbitalSpeed: 0.5,
|
|
513
|
+
rotationSpeed: 1,
|
|
514
|
+
colors: [
|
|
515
|
+
[1, 0.2, 0.2],
|
|
516
|
+
// Red
|
|
517
|
+
[0.2, 1, 0.2],
|
|
518
|
+
// Green
|
|
519
|
+
[0.2, 0.2, 1]
|
|
520
|
+
// Blue
|
|
521
|
+
]
|
|
522
|
+
},
|
|
523
|
+
triangles: {
|
|
524
|
+
count: 2,
|
|
525
|
+
scale: 0.3,
|
|
526
|
+
baseRadius: 0.3,
|
|
527
|
+
radiusVariation: 0.15,
|
|
528
|
+
radiusSpeed: 0.4,
|
|
529
|
+
orbitalSpeed: 0.7,
|
|
530
|
+
rotationSpeed: 1.5,
|
|
531
|
+
rotationDirection: -1,
|
|
532
|
+
// -1 for reverse
|
|
533
|
+
colors: [
|
|
534
|
+
[1, 1, 0.2],
|
|
535
|
+
// Yellow
|
|
536
|
+
[0.2, 1, 1]
|
|
537
|
+
// Cyan
|
|
538
|
+
]
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
function getBackgroundColor(elapsedSeconds) {
|
|
542
|
+
const cfg = ANIMATION_CONFIG.background;
|
|
543
|
+
return {
|
|
544
|
+
r: cfg.offsetR + cfg.amplitudeR * Math.sin(elapsedSeconds * cfg.speedR),
|
|
545
|
+
g: cfg.offsetG + cfg.amplitudeG * Math.cos(elapsedSeconds * cfg.speedG),
|
|
546
|
+
b: cfg.offsetB + cfg.amplitudeB * Math.sin(elapsedSeconds * cfg.speedB),
|
|
547
|
+
a: 1
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function getQuadTransforms(elapsedSeconds) {
|
|
551
|
+
const cfg = ANIMATION_CONFIG.quads;
|
|
552
|
+
const transforms = [];
|
|
553
|
+
for (let i = 0; i < cfg.count; i++) {
|
|
554
|
+
const angle = i / cfg.count * Math.PI * 2 + elapsedSeconds * cfg.orbitalSpeed;
|
|
555
|
+
const radius = cfg.baseRadius + cfg.radiusVariation * Math.sin(elapsedSeconds * cfg.radiusSpeed + i);
|
|
556
|
+
const posX = radius * Math.cos(angle);
|
|
557
|
+
const posY = radius * Math.sin(angle);
|
|
558
|
+
const rotation = elapsedSeconds * cfg.rotationSpeed + i * Math.PI * 2 / cfg.count;
|
|
559
|
+
const cos = Math.cos(rotation);
|
|
560
|
+
const sin = Math.sin(rotation);
|
|
561
|
+
const matrix = new Float32Array([
|
|
562
|
+
cos * cfg.scale,
|
|
563
|
+
sin * cfg.scale,
|
|
564
|
+
0,
|
|
565
|
+
0,
|
|
566
|
+
-sin * cfg.scale,
|
|
567
|
+
cos * cfg.scale,
|
|
568
|
+
0,
|
|
569
|
+
0,
|
|
570
|
+
0,
|
|
571
|
+
0,
|
|
572
|
+
1,
|
|
573
|
+
0,
|
|
574
|
+
posX,
|
|
575
|
+
posY,
|
|
576
|
+
0,
|
|
577
|
+
1
|
|
578
|
+
]);
|
|
579
|
+
transforms.push({
|
|
580
|
+
matrix,
|
|
581
|
+
color: cfg.colors[i],
|
|
582
|
+
index: i
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return transforms;
|
|
586
|
+
}
|
|
587
|
+
function getTriangleTransforms(elapsedSeconds) {
|
|
588
|
+
const cfg = ANIMATION_CONFIG.triangles;
|
|
589
|
+
const transforms = [];
|
|
590
|
+
for (let i = 0; i < cfg.count; i++) {
|
|
591
|
+
const angle = i / cfg.count * Math.PI * 2 + elapsedSeconds * cfg.orbitalSpeed;
|
|
592
|
+
const radius = cfg.baseRadius + cfg.radiusVariation * Math.cos(elapsedSeconds * cfg.radiusSpeed + i);
|
|
593
|
+
const posX = radius * Math.cos(angle);
|
|
594
|
+
const posY = radius * Math.sin(angle);
|
|
595
|
+
const rotation = cfg.rotationDirection * elapsedSeconds * cfg.rotationSpeed + i * Math.PI * 2 / cfg.count;
|
|
596
|
+
const cos = Math.cos(rotation);
|
|
597
|
+
const sin = Math.sin(rotation);
|
|
598
|
+
const matrix = new Float32Array([
|
|
599
|
+
cos * cfg.scale,
|
|
600
|
+
sin * cfg.scale,
|
|
601
|
+
0,
|
|
602
|
+
0,
|
|
603
|
+
-sin * cfg.scale,
|
|
604
|
+
cos * cfg.scale,
|
|
605
|
+
0,
|
|
606
|
+
0,
|
|
607
|
+
0,
|
|
608
|
+
0,
|
|
609
|
+
1,
|
|
610
|
+
0,
|
|
611
|
+
posX,
|
|
612
|
+
posY,
|
|
613
|
+
0,
|
|
614
|
+
1
|
|
615
|
+
]);
|
|
616
|
+
transforms.push({
|
|
617
|
+
matrix,
|
|
618
|
+
color: cfg.colors[i],
|
|
619
|
+
index: i
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return transforms;
|
|
623
|
+
}
|
|
624
|
+
const scene = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
625
|
+
__proto__: null,
|
|
626
|
+
ANIMATION_CONFIG,
|
|
627
|
+
GEOMETRY,
|
|
628
|
+
PROJECTION_CONFIG,
|
|
629
|
+
PROJECTION_ENTITIES,
|
|
630
|
+
SCENE_CONFIG,
|
|
631
|
+
SHADERS_V2,
|
|
632
|
+
TEXTURE_CONFIG,
|
|
633
|
+
getBackgroundColor,
|
|
634
|
+
getQuadTransforms,
|
|
635
|
+
getTriangleTransforms
|
|
636
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
637
|
+
function initializeGameProjection() {
|
|
638
|
+
const projection = createDefaultProjection();
|
|
639
|
+
console.log("✓ Game projection initialized:");
|
|
640
|
+
console.log(
|
|
641
|
+
` Tile dimensions: ${projection.tileWidth}x${projection.tileHeight}`
|
|
642
|
+
);
|
|
643
|
+
console.log(` Height scale: ${projection.zScale}`);
|
|
644
|
+
return projection;
|
|
645
|
+
}
|
|
646
|
+
function updateEntityScreenPosition(entity, projection) {
|
|
647
|
+
const screenPos = gridToScreen(entity.gridPos, projection);
|
|
648
|
+
const cellCenter = getCellCenterOffset(projection);
|
|
649
|
+
entity.screenPos = {
|
|
650
|
+
xscreen: screenPos.xscreen + cellCenter.xscreen,
|
|
651
|
+
yscreen: screenPos.yscreen + cellCenter.yscreen
|
|
652
|
+
};
|
|
653
|
+
console.log(
|
|
654
|
+
`✓ ${entity.name} at grid (${entity.gridPos.xgrid}, ${entity.gridPos.ygrid}, ${entity.gridPos.zheight}) → screen (${entity.screenPos.xscreen}, ${entity.screenPos.yscreen})`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
function createEntity(id, name, gridPos, projection) {
|
|
658
|
+
const entity = { id, name, gridPos };
|
|
659
|
+
updateEntityScreenPosition(entity, projection);
|
|
660
|
+
return entity;
|
|
661
|
+
}
|
|
662
|
+
function calculateGridDistance(from, to) {
|
|
663
|
+
return Math.abs(to.xgrid - from.xgrid) + Math.abs(to.ygrid - from.ygrid);
|
|
664
|
+
}
|
|
665
|
+
function calculate3DDistance(from, to) {
|
|
666
|
+
const dx = to.xgrid - from.xgrid;
|
|
667
|
+
const dy = to.ygrid - from.ygrid;
|
|
668
|
+
const dz = to.zheight - from.zheight;
|
|
669
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
670
|
+
}
|
|
671
|
+
function moveEntityTowards(entity, target, projection) {
|
|
672
|
+
const dx = Math.sign(target.xgrid - entity.gridPos.xgrid);
|
|
673
|
+
const dy = Math.sign(target.ygrid - entity.gridPos.ygrid);
|
|
674
|
+
if (dx !== 0) entity.gridPos.xgrid += dx;
|
|
675
|
+
if (dy !== 0) entity.gridPos.ygrid += dy;
|
|
676
|
+
updateEntityScreenPosition(entity, projection);
|
|
677
|
+
const distance = calculateGridDistance(entity.gridPos, target);
|
|
678
|
+
console.log(`✓ ${entity.name} moved, distance to target: ${distance} cells`);
|
|
679
|
+
}
|
|
680
|
+
function calculateRenderPriority(gridCoord) {
|
|
681
|
+
return gridCoord.xgrid + gridCoord.ygrid;
|
|
682
|
+
}
|
|
683
|
+
function sortEntitiesByDepth(entities) {
|
|
684
|
+
return [...entities].sort(
|
|
685
|
+
(a, b) => calculateRenderPriority(a.gridPos) - calculateRenderPriority(b.gridPos)
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
function createTerrainHeightMap() {
|
|
689
|
+
return (x, y) => {
|
|
690
|
+
const centerX = 10;
|
|
691
|
+
const centerY = 10;
|
|
692
|
+
const radius = 5;
|
|
693
|
+
const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
|
|
694
|
+
if (distance < radius) {
|
|
695
|
+
return Math.max(0, 5 * (1 - distance / radius));
|
|
696
|
+
}
|
|
697
|
+
return 0;
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function placeEntityOnTerrain(entity, heightMap, projection) {
|
|
701
|
+
entity.gridPos.zheight = heightMap(
|
|
702
|
+
entity.gridPos.xgrid,
|
|
703
|
+
entity.gridPos.ygrid
|
|
704
|
+
);
|
|
705
|
+
updateEntityScreenPosition(entity, projection);
|
|
706
|
+
console.log(
|
|
707
|
+
`✓ ${entity.name} placed on terrain at height ${entity.gridPos.zheight}`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
function createCamera(centerGrid, viewWidthPixels, viewHeightPixels, projection) {
|
|
711
|
+
return {
|
|
712
|
+
center: centerGrid,
|
|
713
|
+
viewWidth: viewWidthPixels,
|
|
714
|
+
viewHeight: viewHeightPixels
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function isEntityInView(entity, camera, projection, margin = 50) {
|
|
718
|
+
if (!entity.screenPos) return false;
|
|
719
|
+
const cameraScreen = gridToScreen(camera.center, projection);
|
|
720
|
+
const halfWidth = camera.viewWidth / 2 + margin;
|
|
721
|
+
const halfHeight = camera.viewHeight / 2 + margin;
|
|
722
|
+
return Math.abs(entity.screenPos.xscreen - cameraScreen.xscreen) < halfWidth && Math.abs(entity.screenPos.yscreen - cameraScreen.yscreen) < halfHeight;
|
|
723
|
+
}
|
|
724
|
+
function runExampleScenario() {
|
|
725
|
+
console.log("\n" + "=".repeat(60));
|
|
726
|
+
console.log("🎮 2.5D Projection System - Example Scenario");
|
|
727
|
+
console.log("=".repeat(60));
|
|
728
|
+
const projection = initializeGameProjection();
|
|
729
|
+
console.log("\n📍 Creating game entities:");
|
|
730
|
+
const player = createEntity(
|
|
731
|
+
"player",
|
|
732
|
+
"Player",
|
|
733
|
+
{ xgrid: 5, ygrid: 5, zheight: 0 },
|
|
734
|
+
projection
|
|
735
|
+
);
|
|
736
|
+
const enemy = createEntity(
|
|
737
|
+
"enemy1",
|
|
738
|
+
"Enemy",
|
|
739
|
+
{ xgrid: 10, ygrid: 8, zheight: 0 },
|
|
740
|
+
projection
|
|
741
|
+
);
|
|
742
|
+
const chest = createEntity(
|
|
743
|
+
"chest1",
|
|
744
|
+
"Treasure Chest",
|
|
745
|
+
{ xgrid: 8, ygrid: 6, zheight: 0 },
|
|
746
|
+
projection
|
|
747
|
+
);
|
|
748
|
+
console.log("\n🚶 Movement example:");
|
|
749
|
+
for (let i = 0; i < 3; i++) {
|
|
750
|
+
moveEntityTowards(player, chest.gridPos, projection);
|
|
751
|
+
}
|
|
752
|
+
console.log("\n📏 Distance calculations:");
|
|
753
|
+
const dist = calculateGridDistance(player.gridPos, enemy.gridPos);
|
|
754
|
+
const dist3d = calculate3DDistance(player.gridPos, enemy.gridPos);
|
|
755
|
+
console.log(` Manhattan distance: ${dist} cells`);
|
|
756
|
+
console.log(` 3D distance: ${dist3d.toFixed(2)} units`);
|
|
757
|
+
console.log("\n🎨 Render order (depth sorting):");
|
|
758
|
+
const entities = [player, enemy, chest];
|
|
759
|
+
const sorted = sortEntitiesByDepth(entities);
|
|
760
|
+
sorted.forEach((e, i) => {
|
|
761
|
+
console.log(
|
|
762
|
+
` ${i + 1}. ${e.name} (priority: ${calculateRenderPriority(e.gridPos)})`
|
|
763
|
+
);
|
|
764
|
+
});
|
|
765
|
+
console.log("\n🏔️ Terrain integration:");
|
|
766
|
+
const heightMap = createTerrainHeightMap();
|
|
767
|
+
placeEntityOnTerrain(enemy, heightMap, projection);
|
|
768
|
+
console.log("\n📷 Camera example:");
|
|
769
|
+
const camera = createCamera(player.gridPos, 800, 600);
|
|
770
|
+
console.log(
|
|
771
|
+
` Camera centered at grid (${camera.center.xgrid}, ${camera.center.ygrid})`
|
|
772
|
+
);
|
|
773
|
+
console.log(` View size: ${camera.viewWidth}x${camera.viewHeight} pixels`);
|
|
774
|
+
console.log(
|
|
775
|
+
` Player in view: ${isEntityInView(player, camera, projection)}`
|
|
776
|
+
);
|
|
777
|
+
console.log(` Enemy in view: ${isEntityInView(enemy, camera, projection)}`);
|
|
778
|
+
console.log("\n" + "=".repeat(60));
|
|
779
|
+
}
|
|
780
|
+
const BASIC_SHADER = {
|
|
781
|
+
vertex: `
|
|
782
|
+
attribute vec3 aPosition;
|
|
783
|
+
attribute vec2 aTexCoord;
|
|
784
|
+
|
|
785
|
+
varying vec2 vTexCoord;
|
|
786
|
+
|
|
787
|
+
uniform mat4 uMatrix;
|
|
788
|
+
|
|
789
|
+
void main() {
|
|
790
|
+
gl_Position = uMatrix * vec4(aPosition, 1.0);
|
|
791
|
+
vTexCoord = aTexCoord;
|
|
792
|
+
}
|
|
793
|
+
`,
|
|
794
|
+
fragment: `
|
|
795
|
+
varying vec2 vTexCoord;
|
|
796
|
+
uniform sampler2D uTexture;
|
|
797
|
+
uniform vec3 uColor;
|
|
798
|
+
|
|
799
|
+
void main() {
|
|
800
|
+
vec4 texColor = texture2D(uTexture, vTexCoord);
|
|
801
|
+
gl_FragColor = vec4(texColor.rgb * uColor, texColor.a);
|
|
802
|
+
}
|
|
803
|
+
`,
|
|
804
|
+
uniforms: ["uMatrix", "uTexture", "uColor"],
|
|
805
|
+
attributes: ["aPosition", "aTexCoord"]
|
|
806
|
+
};
|
|
807
|
+
const GLOW_SHADER = {
|
|
808
|
+
vertex: `
|
|
809
|
+
attribute vec3 aPosition;
|
|
810
|
+
attribute vec2 aTexCoord;
|
|
811
|
+
|
|
812
|
+
varying vec2 vTexCoord;
|
|
813
|
+
varying float vDistance;
|
|
814
|
+
|
|
815
|
+
uniform mat4 uMatrix;
|
|
816
|
+
|
|
817
|
+
void main() {
|
|
818
|
+
gl_Position = uMatrix * vec4(aPosition, 1.0);
|
|
819
|
+
vTexCoord = aTexCoord;
|
|
820
|
+
// Distance from center for glow falloff
|
|
821
|
+
vDistance = length(aTexCoord - vec2(0.5, 0.5));
|
|
822
|
+
}
|
|
823
|
+
`,
|
|
824
|
+
fragment: `
|
|
825
|
+
varying vec2 vTexCoord;
|
|
826
|
+
varying float vDistance;
|
|
827
|
+
uniform sampler2D uTexture;
|
|
828
|
+
uniform vec3 uColor;
|
|
829
|
+
uniform float uGlowIntensity;
|
|
830
|
+
|
|
831
|
+
void main() {
|
|
832
|
+
vec4 texColor = texture2D(uTexture, vTexCoord);
|
|
833
|
+
float glow = 1.0 - vDistance * 2.0;
|
|
834
|
+
glow = max(0.0, glow);
|
|
835
|
+
vec3 glowColor = texColor.rgb * uColor * glow * uGlowIntensity;
|
|
836
|
+
gl_FragColor = vec4(glowColor, texColor.a);
|
|
837
|
+
}
|
|
838
|
+
`,
|
|
839
|
+
uniforms: ["uMatrix", "uTexture", "uColor", "uGlowIntensity"],
|
|
840
|
+
attributes: ["aPosition", "aTexCoord"]
|
|
841
|
+
};
|
|
842
|
+
const WAVE_SHADER = {
|
|
843
|
+
vertex: `
|
|
844
|
+
attribute vec3 aPosition;
|
|
845
|
+
attribute vec2 aTexCoord;
|
|
846
|
+
|
|
847
|
+
varying vec2 vTexCoord;
|
|
848
|
+
|
|
849
|
+
uniform mat4 uMatrix;
|
|
850
|
+
uniform float uTime;
|
|
851
|
+
uniform float uWaveAmount;
|
|
852
|
+
|
|
853
|
+
void main() {
|
|
854
|
+
vec3 pos = aPosition;
|
|
855
|
+
// Apply wave deformation
|
|
856
|
+
pos.y += sin(aPosition.x * 3.14159 + uTime) * uWaveAmount;
|
|
857
|
+
pos.x += cos(aPosition.y * 3.14159 + uTime * 0.7) * uWaveAmount * 0.5;
|
|
858
|
+
|
|
859
|
+
gl_Position = uMatrix * vec4(pos, 1.0);
|
|
860
|
+
vTexCoord = aTexCoord;
|
|
861
|
+
}
|
|
862
|
+
`,
|
|
863
|
+
fragment: `
|
|
864
|
+
varying vec2 vTexCoord;
|
|
865
|
+
uniform sampler2D uTexture;
|
|
866
|
+
uniform vec3 uColor;
|
|
867
|
+
|
|
868
|
+
void main() {
|
|
869
|
+
vec4 texColor = texture2D(uTexture, vTexCoord);
|
|
870
|
+
gl_FragColor = vec4(texColor.rgb * uColor, texColor.a);
|
|
871
|
+
}
|
|
872
|
+
`,
|
|
873
|
+
uniforms: ["uMatrix", "uTexture", "uColor", "uTime", "uWaveAmount"],
|
|
874
|
+
attributes: ["aPosition", "aTexCoord"]
|
|
875
|
+
};
|
|
876
|
+
const NEON_SHADER = {
|
|
877
|
+
vertex: `
|
|
878
|
+
attribute vec3 aPosition;
|
|
879
|
+
attribute vec2 aTexCoord;
|
|
880
|
+
|
|
881
|
+
varying vec2 vTexCoord;
|
|
882
|
+
varying vec3 vNormal;
|
|
883
|
+
|
|
884
|
+
uniform mat4 uMatrix;
|
|
885
|
+
|
|
886
|
+
void main() {
|
|
887
|
+
gl_Position = uMatrix * vec4(aPosition, 1.0);
|
|
888
|
+
vTexCoord = aTexCoord;
|
|
889
|
+
// Simple normal based on position
|
|
890
|
+
vNormal = normalize(aPosition);
|
|
891
|
+
}
|
|
892
|
+
`,
|
|
893
|
+
fragment: `
|
|
894
|
+
varying vec2 vTexCoord;
|
|
895
|
+
varying vec3 vNormal;
|
|
896
|
+
uniform sampler2D uTexture;
|
|
897
|
+
uniform vec3 uColor;
|
|
898
|
+
|
|
899
|
+
void main() {
|
|
900
|
+
vec4 texColor = texture2D(uTexture, vTexCoord);
|
|
901
|
+
|
|
902
|
+
// High contrast
|
|
903
|
+
vec3 enhanced = pow(texColor.rgb * uColor, vec3(0.5));
|
|
904
|
+
|
|
905
|
+
// Edge highlight
|
|
906
|
+
float edge = abs(vNormal.x) + abs(vNormal.y);
|
|
907
|
+
edge = smoothstep(0.3, 1.0, edge) * 0.5;
|
|
908
|
+
|
|
909
|
+
vec3 neonColor = enhanced + edge * uColor;
|
|
910
|
+
gl_FragColor = vec4(neonColor, texColor.a);
|
|
911
|
+
}
|
|
912
|
+
`,
|
|
913
|
+
uniforms: ["uMatrix", "uTexture", "uColor"],
|
|
914
|
+
attributes: ["aPosition", "aTexCoord"]
|
|
915
|
+
};
|
|
916
|
+
const HOLOGRAM_SHADER = {
|
|
917
|
+
vertex: `
|
|
918
|
+
attribute vec3 aPosition;
|
|
919
|
+
attribute vec2 aTexCoord;
|
|
920
|
+
|
|
921
|
+
varying vec2 vTexCoord;
|
|
922
|
+
|
|
923
|
+
uniform mat4 uMatrix;
|
|
924
|
+
|
|
925
|
+
void main() {
|
|
926
|
+
gl_Position = uMatrix * vec4(aPosition, 1.0);
|
|
927
|
+
vTexCoord = aTexCoord;
|
|
928
|
+
}
|
|
929
|
+
`,
|
|
930
|
+
fragment: `
|
|
931
|
+
varying vec2 vTexCoord;
|
|
932
|
+
uniform sampler2D uTexture;
|
|
933
|
+
uniform vec3 uColor;
|
|
934
|
+
uniform float uTime;
|
|
935
|
+
|
|
936
|
+
void main() {
|
|
937
|
+
vec4 texColor = texture2D(uTexture, vTexCoord);
|
|
938
|
+
|
|
939
|
+
// Scan lines
|
|
940
|
+
float scanLine = sin(vTexCoord.y * 50.0) * 0.1 + 0.9;
|
|
941
|
+
|
|
942
|
+
// Flicker transparency
|
|
943
|
+
float flicker = 0.8 + 0.2 * sin(uTime * 5.0 + vTexCoord.x * 10.0);
|
|
944
|
+
|
|
945
|
+
vec3 hologramColor = texColor.rgb * uColor * scanLine;
|
|
946
|
+
gl_FragColor = vec4(hologramColor, texColor.a * flicker);
|
|
947
|
+
}
|
|
948
|
+
`,
|
|
949
|
+
uniforms: ["uMatrix", "uTexture", "uColor", "uTime"],
|
|
950
|
+
attributes: ["aPosition", "aTexCoord"]
|
|
951
|
+
};
|
|
952
|
+
const CHROMATIC_SHADER = {
|
|
953
|
+
vertex: `
|
|
954
|
+
attribute vec3 aPosition;
|
|
955
|
+
attribute vec2 aTexCoord;
|
|
956
|
+
|
|
957
|
+
varying vec2 vTexCoord;
|
|
958
|
+
|
|
959
|
+
uniform mat4 uMatrix;
|
|
960
|
+
|
|
961
|
+
void main() {
|
|
962
|
+
gl_Position = uMatrix * vec4(aPosition, 1.0);
|
|
963
|
+
vTexCoord = aTexCoord;
|
|
964
|
+
}
|
|
965
|
+
`,
|
|
966
|
+
fragment: `
|
|
967
|
+
varying vec2 vTexCoord;
|
|
968
|
+
uniform sampler2D uTexture;
|
|
969
|
+
uniform vec3 uColor;
|
|
970
|
+
uniform float uAberration;
|
|
971
|
+
|
|
972
|
+
void main() {
|
|
973
|
+
float offset = uAberration * 0.01;
|
|
974
|
+
|
|
975
|
+
float r = texture2D(uTexture, vTexCoord + vec2(offset, 0.0)).r;
|
|
976
|
+
float g = texture2D(uTexture, vTexCoord).g;
|
|
977
|
+
float b = texture2D(uTexture, vTexCoord - vec2(offset, 0.0)).b;
|
|
978
|
+
|
|
979
|
+
vec3 aberrated = vec3(r, g, b) * uColor;
|
|
980
|
+
gl_FragColor = vec4(aberrated, 1.0);
|
|
981
|
+
}
|
|
982
|
+
`,
|
|
983
|
+
uniforms: ["uMatrix", "uTexture", "uColor", "uAberration"],
|
|
984
|
+
attributes: ["aPosition", "aTexCoord"]
|
|
985
|
+
};
|
|
986
|
+
const PSYCHEDELIC_SHADER = {
|
|
987
|
+
vertex: `
|
|
988
|
+
attribute vec3 aPosition;
|
|
989
|
+
attribute vec2 aTexCoord;
|
|
990
|
+
|
|
991
|
+
varying vec2 vTexCoord;
|
|
992
|
+
|
|
993
|
+
uniform mat4 uMatrix;
|
|
994
|
+
|
|
995
|
+
void main() {
|
|
996
|
+
gl_Position = uMatrix * vec4(aPosition, 1.0);
|
|
997
|
+
vTexCoord = aTexCoord;
|
|
998
|
+
}
|
|
999
|
+
`,
|
|
1000
|
+
fragment: `
|
|
1001
|
+
varying vec2 vTexCoord;
|
|
1002
|
+
uniform sampler2D uTexture;
|
|
1003
|
+
uniform float uTime;
|
|
1004
|
+
|
|
1005
|
+
void main() {
|
|
1006
|
+
vec4 texColor = texture2D(uTexture, vTexCoord);
|
|
1007
|
+
|
|
1008
|
+
// Rainbow shift based on position and time
|
|
1009
|
+
float hue = atan(vTexCoord.y - 0.5, vTexCoord.x - 0.5) / 3.14159;
|
|
1010
|
+
hue += uTime;
|
|
1011
|
+
|
|
1012
|
+
float r = sin(hue) * 0.5 + 0.5;
|
|
1013
|
+
float g = sin(hue + 2.094) * 0.5 + 0.5;
|
|
1014
|
+
float b = sin(hue + 4.188) * 0.5 + 0.5;
|
|
1015
|
+
|
|
1016
|
+
vec3 rainbow = vec3(r, g, b);
|
|
1017
|
+
gl_FragColor = vec4(texColor.rgb * rainbow, texColor.a);
|
|
1018
|
+
}
|
|
1019
|
+
`,
|
|
1020
|
+
uniforms: ["uMatrix", "uTexture", "uTime"],
|
|
1021
|
+
attributes: ["aPosition", "aTexCoord"]
|
|
1022
|
+
};
|
|
1023
|
+
const SHADER_LIBRARY = {
|
|
1024
|
+
BASIC: BASIC_SHADER,
|
|
1025
|
+
GLOW: GLOW_SHADER,
|
|
1026
|
+
WAVE: WAVE_SHADER,
|
|
1027
|
+
NEON: NEON_SHADER,
|
|
1028
|
+
HOLOGRAM: HOLOGRAM_SHADER,
|
|
1029
|
+
CHROMATIC: CHROMATIC_SHADER,
|
|
1030
|
+
PSYCHEDELIC: PSYCHEDELIC_SHADER
|
|
1031
|
+
};
|
|
1032
|
+
class BrowserRenderingContext {
|
|
1033
|
+
constructor(options) {
|
|
1034
|
+
this.isBrowser = true;
|
|
1035
|
+
if (!options.canvas) {
|
|
1036
|
+
this.canvas = document.createElement("canvas");
|
|
1037
|
+
document.body.appendChild(this.canvas);
|
|
1038
|
+
} else {
|
|
1039
|
+
this.canvas = options.canvas;
|
|
1040
|
+
}
|
|
1041
|
+
this.width = options.width;
|
|
1042
|
+
this.height = options.height;
|
|
1043
|
+
this.canvas.width = this.width;
|
|
1044
|
+
this.canvas.height = this.height;
|
|
1045
|
+
const contextAttributes = {
|
|
1046
|
+
alpha: false,
|
|
1047
|
+
...options.contextAttributes
|
|
1048
|
+
};
|
|
1049
|
+
const glContext = this.canvas.getContext("webgl", contextAttributes);
|
|
1050
|
+
if (!glContext) {
|
|
1051
|
+
throw new Error("Failed to initialize WebGL context in browser");
|
|
1052
|
+
}
|
|
1053
|
+
this.glContext = glContext;
|
|
1054
|
+
}
|
|
1055
|
+
resize(width, height) {
|
|
1056
|
+
this.width = width;
|
|
1057
|
+
this.height = height;
|
|
1058
|
+
this.canvas.width = width;
|
|
1059
|
+
this.canvas.height = height;
|
|
1060
|
+
this.glContext.viewport(0, 0, width, height);
|
|
1061
|
+
}
|
|
1062
|
+
getViewport() {
|
|
1063
|
+
return { width: this.width, height: this.height };
|
|
1064
|
+
}
|
|
1065
|
+
clear(color) {
|
|
1066
|
+
if (color) {
|
|
1067
|
+
this.glContext.clearColor(color.r, color.g, color.b, color.a);
|
|
1068
|
+
}
|
|
1069
|
+
this.glContext.clear(
|
|
1070
|
+
this.glContext.COLOR_BUFFER_BIT | this.glContext.DEPTH_BUFFER_BIT
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
present() {
|
|
1074
|
+
}
|
|
1075
|
+
dispose() {
|
|
1076
|
+
if (this.canvas.parentElement) {
|
|
1077
|
+
this.canvas.parentElement.removeChild(this.canvas);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Get the underlying canvas element (browser-specific)
|
|
1082
|
+
*/
|
|
1083
|
+
getCanvas() {
|
|
1084
|
+
return this.canvas;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
class NodeRenderingContext {
|
|
1088
|
+
constructor(options) {
|
|
1089
|
+
this.isBrowser = false;
|
|
1090
|
+
this.width = options.width;
|
|
1091
|
+
this.height = options.height;
|
|
1092
|
+
const glContext = createGL(this.width, this.height, {
|
|
1093
|
+
preserveDrawingBuffer: options.preserveDrawingBuffer ?? true,
|
|
1094
|
+
...options.contextAttributes
|
|
1095
|
+
});
|
|
1096
|
+
if (!glContext) {
|
|
1097
|
+
throw new Error("Failed to initialize WebGL context in Node.js");
|
|
1098
|
+
}
|
|
1099
|
+
this.glContext = glContext;
|
|
1100
|
+
}
|
|
1101
|
+
resize(width, height) {
|
|
1102
|
+
this.width = width;
|
|
1103
|
+
this.height = height;
|
|
1104
|
+
console.warn(
|
|
1105
|
+
"NodeRenderingContext: Resize requested but not supported. Consider recreating context."
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
getViewport() {
|
|
1109
|
+
return { width: this.width, height: this.height };
|
|
1110
|
+
}
|
|
1111
|
+
clear(color) {
|
|
1112
|
+
if (color) {
|
|
1113
|
+
this.glContext.clearColor(color.r, color.g, color.b, color.a);
|
|
1114
|
+
}
|
|
1115
|
+
this.glContext.clear(
|
|
1116
|
+
this.glContext.COLOR_BUFFER_BIT | this.glContext.DEPTH_BUFFER_BIT
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
present() {
|
|
1120
|
+
this.glContext.flush();
|
|
1121
|
+
}
|
|
1122
|
+
dispose() {
|
|
1123
|
+
this.glContext.flush();
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Read the current framebuffer contents as RGBA pixel data
|
|
1127
|
+
* Used for capturing frames for display or saving
|
|
1128
|
+
*/
|
|
1129
|
+
readPixels() {
|
|
1130
|
+
const pixelData = new Uint8Array(this.width * this.height * 4);
|
|
1131
|
+
this.glContext.readPixels(
|
|
1132
|
+
0,
|
|
1133
|
+
0,
|
|
1134
|
+
this.width,
|
|
1135
|
+
this.height,
|
|
1136
|
+
this.glContext.RGBA,
|
|
1137
|
+
this.glContext.UNSIGNED_BYTE,
|
|
1138
|
+
pixelData
|
|
1139
|
+
);
|
|
1140
|
+
return pixelData;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
class RenderingContextFactory {
|
|
1144
|
+
/**
|
|
1145
|
+
* Detect if running in a browser environment
|
|
1146
|
+
*/
|
|
1147
|
+
static isBrowserEnvironment() {
|
|
1148
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Create a rendering context appropriate for the current environment
|
|
1152
|
+
*/
|
|
1153
|
+
static createContext(options) {
|
|
1154
|
+
if (this.isBrowserEnvironment()) {
|
|
1155
|
+
return new BrowserRenderingContext(options);
|
|
1156
|
+
} else {
|
|
1157
|
+
return new NodeRenderingContext(options);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Create a browser-specific rendering context
|
|
1162
|
+
*/
|
|
1163
|
+
static createBrowserContext(options) {
|
|
1164
|
+
return new BrowserRenderingContext(options);
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Create a Node.js-specific rendering context
|
|
1168
|
+
*/
|
|
1169
|
+
static createNodeContext(options) {
|
|
1170
|
+
return new NodeRenderingContext(options);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
class Shader {
|
|
1174
|
+
/**
|
|
1175
|
+
* Create a new shader program
|
|
1176
|
+
* @param gl WebGL rendering context
|
|
1177
|
+
* @param vertexSource Raw vertex shader source code
|
|
1178
|
+
* @param fragmentSource Raw fragment shader source code
|
|
1179
|
+
* @param isBrowser Whether running in browser environment (affects precision header)
|
|
1180
|
+
*/
|
|
1181
|
+
constructor(gl2, vertexSource, fragmentSource, isBrowser) {
|
|
1182
|
+
this.gl = gl2;
|
|
1183
|
+
const processedVertexSource = this.injectPrecisionHeader(
|
|
1184
|
+
vertexSource,
|
|
1185
|
+
isBrowser
|
|
1186
|
+
);
|
|
1187
|
+
const processedFragmentSource = this.injectPrecisionHeader(
|
|
1188
|
+
fragmentSource,
|
|
1189
|
+
isBrowser
|
|
1190
|
+
);
|
|
1191
|
+
this.vertexShader = this.compileShader(
|
|
1192
|
+
processedVertexSource,
|
|
1193
|
+
gl2.VERTEX_SHADER
|
|
1194
|
+
);
|
|
1195
|
+
this.fragmentShader = this.compileShader(
|
|
1196
|
+
processedFragmentSource,
|
|
1197
|
+
gl2.FRAGMENT_SHADER
|
|
1198
|
+
);
|
|
1199
|
+
this.program = this.linkProgram(this.vertexShader, this.fragmentShader);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Inject precision header for ES and desktop OpenGL differences
|
|
1203
|
+
* @param source Original shader source
|
|
1204
|
+
* @param isBrowser Whether in browser (WebGL ES) or Node (desktop OpenGL)
|
|
1205
|
+
* @returns Processed shader source with precision header
|
|
1206
|
+
*/
|
|
1207
|
+
injectPrecisionHeader(source, isBrowser) {
|
|
1208
|
+
if (source.includes("#ifdef GL_ES") || source.includes("precision")) {
|
|
1209
|
+
return source;
|
|
1210
|
+
}
|
|
1211
|
+
if (isBrowser) {
|
|
1212
|
+
const precisionHeader = `#ifdef GL_ES
|
|
1213
|
+
precision highp float;
|
|
1214
|
+
#endif
|
|
1215
|
+
`;
|
|
1216
|
+
return precisionHeader + source;
|
|
1217
|
+
} else {
|
|
1218
|
+
const precisionHeader = `#ifdef GL_ES
|
|
1219
|
+
precision highp float;
|
|
1220
|
+
#endif
|
|
1221
|
+
`;
|
|
1222
|
+
return precisionHeader + source;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Compile a single shader (vertex or fragment)
|
|
1227
|
+
* @param source Shader source code
|
|
1228
|
+
* @param type gl.VERTEX_SHADER or gl.FRAGMENT_SHADER
|
|
1229
|
+
* @returns Compiled shader
|
|
1230
|
+
*/
|
|
1231
|
+
compileShader(source, type) {
|
|
1232
|
+
const shader2 = this.gl.createShader(type);
|
|
1233
|
+
if (!shader2) {
|
|
1234
|
+
throw new Error(`Failed to create shader of type ${type}`);
|
|
1235
|
+
}
|
|
1236
|
+
this.gl.shaderSource(shader2, source);
|
|
1237
|
+
this.gl.compileShader(shader2);
|
|
1238
|
+
const compiled = this.gl.getShaderParameter(shader2, this.gl.COMPILE_STATUS);
|
|
1239
|
+
if (!compiled) {
|
|
1240
|
+
const infoLog = this.gl.getShaderInfoLog(shader2);
|
|
1241
|
+
const shaderType = type === this.gl.VERTEX_SHADER ? "vertex" : "fragment";
|
|
1242
|
+
this.gl.deleteShader(shader2);
|
|
1243
|
+
throw new Error(
|
|
1244
|
+
`Failed to compile ${shaderType} shader:
|
|
1245
|
+
${infoLog}
|
|
1246
|
+
|
|
1247
|
+
Source:
|
|
1248
|
+
${source}`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
return shader2;
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Link vertex and fragment shaders into a program
|
|
1255
|
+
* @param vertexShader Compiled vertex shader
|
|
1256
|
+
* @param fragmentShader Compiled fragment shader
|
|
1257
|
+
* @returns Linked shader program
|
|
1258
|
+
*/
|
|
1259
|
+
linkProgram(vertexShader, fragmentShader) {
|
|
1260
|
+
const program = this.gl.createProgram();
|
|
1261
|
+
if (!program) {
|
|
1262
|
+
throw new Error("Failed to create shader program");
|
|
1263
|
+
}
|
|
1264
|
+
this.gl.attachShader(program, vertexShader);
|
|
1265
|
+
this.gl.attachShader(program, fragmentShader);
|
|
1266
|
+
this.gl.linkProgram(program);
|
|
1267
|
+
const linked = this.gl.getProgramParameter(program, this.gl.LINK_STATUS);
|
|
1268
|
+
if (!linked) {
|
|
1269
|
+
const infoLog = this.gl.getProgramInfoLog(program);
|
|
1270
|
+
this.gl.deleteProgram(program);
|
|
1271
|
+
this.gl.deleteShader(vertexShader);
|
|
1272
|
+
this.gl.deleteShader(fragmentShader);
|
|
1273
|
+
throw new Error(`Failed to link shader program:
|
|
1274
|
+
${infoLog}`);
|
|
1275
|
+
}
|
|
1276
|
+
return program;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Get the compiled shader program
|
|
1280
|
+
*/
|
|
1281
|
+
getProgram() {
|
|
1282
|
+
return this.program;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Get uniform location by name
|
|
1286
|
+
* @param name Uniform variable name
|
|
1287
|
+
*/
|
|
1288
|
+
getUniformLocation(name) {
|
|
1289
|
+
return this.gl.getUniformLocation(this.program, name);
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Get attribute location by name
|
|
1293
|
+
* @param name Attribute variable name
|
|
1294
|
+
*/
|
|
1295
|
+
getAttributeLocation(name) {
|
|
1296
|
+
return this.gl.getAttribLocation(this.program, name);
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Use this shader program
|
|
1300
|
+
*/
|
|
1301
|
+
use() {
|
|
1302
|
+
this.gl.useProgram(this.program);
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Clean up shader resources
|
|
1306
|
+
*/
|
|
1307
|
+
dispose() {
|
|
1308
|
+
this.gl.deleteProgram(this.program);
|
|
1309
|
+
this.gl.deleteShader(this.vertexShader);
|
|
1310
|
+
this.gl.deleteShader(this.fragmentShader);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const shader = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1314
|
+
__proto__: null,
|
|
1315
|
+
Shader
|
|
1316
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1317
|
+
class GraphicsDevice {
|
|
1318
|
+
constructor(width, height) {
|
|
1319
|
+
this.context = RenderingContextFactory.createContext({
|
|
1320
|
+
width,
|
|
1321
|
+
height,
|
|
1322
|
+
preserveDrawingBuffer: true
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Get the underlying WebGL rendering context
|
|
1327
|
+
*/
|
|
1328
|
+
getGLContext() {
|
|
1329
|
+
return this.context.glContext;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Get the rendering context
|
|
1333
|
+
*/
|
|
1334
|
+
getRenderingContext() {
|
|
1335
|
+
return this.context;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Get current width
|
|
1339
|
+
*/
|
|
1340
|
+
getWidth() {
|
|
1341
|
+
return this.context.width;
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Get current height
|
|
1345
|
+
*/
|
|
1346
|
+
getHeight() {
|
|
1347
|
+
return this.context.height;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Get viewport dimensions
|
|
1351
|
+
*/
|
|
1352
|
+
getViewport() {
|
|
1353
|
+
return this.context.getViewport();
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Check if running in browser
|
|
1357
|
+
*/
|
|
1358
|
+
isBrowser() {
|
|
1359
|
+
return this.context.isBrowser;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Resize the graphics device
|
|
1363
|
+
*/
|
|
1364
|
+
resize(width, height) {
|
|
1365
|
+
this.context.resize(width, height);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Clear the rendering surface
|
|
1369
|
+
*/
|
|
1370
|
+
clear(color) {
|
|
1371
|
+
this.context.clear(color);
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Present the rendered frame
|
|
1375
|
+
*/
|
|
1376
|
+
present() {
|
|
1377
|
+
this.context.present();
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Cleanup and release resources
|
|
1381
|
+
*/
|
|
1382
|
+
dispose() {
|
|
1383
|
+
this.context.dispose();
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Create a shader program
|
|
1387
|
+
* @param vertexSource Vertex shader source code
|
|
1388
|
+
* @param fragmentSource Fragment shader source code
|
|
1389
|
+
* @returns Compiled and linked shader program
|
|
1390
|
+
*/
|
|
1391
|
+
createShader(vertexSource, fragmentSource) {
|
|
1392
|
+
return new Shader(
|
|
1393
|
+
this.context.glContext,
|
|
1394
|
+
vertexSource,
|
|
1395
|
+
fragmentSource,
|
|
1396
|
+
this.context.isBrowser
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const grahpicDevice = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1401
|
+
__proto__: null,
|
|
1402
|
+
GraphicsDevice
|
|
1403
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1404
|
+
console.log(
|
|
1405
|
+
"🩸 Bloody Engine - Texture & Shader Demo + Projection Visualization + Resource Loader"
|
|
1406
|
+
);
|
|
1407
|
+
const WIDTH = SCENE_CONFIG.width;
|
|
1408
|
+
const HEIGHT = SCENE_CONFIG.height;
|
|
1409
|
+
async function runResourceLoaderDemo() {
|
|
1410
|
+
console.log("\n" + "=".repeat(60));
|
|
1411
|
+
console.log("📦 RESOURCE LOADER DEMO (Node.js)");
|
|
1412
|
+
console.log("=".repeat(60));
|
|
1413
|
+
const { ResourceLoaderFactory, Environment } = await import("./resource-loader-factory-DQ-PAVcN.js");
|
|
1414
|
+
const { createResourcePipeline } = await import("./resource-pipeline-Dac9qRso.js");
|
|
1415
|
+
const { GraphicsDevice: GraphicsDevice2 } = await Promise.resolve().then(() => grahpicDevice);
|
|
1416
|
+
const { Shader: Shader2 } = await Promise.resolve().then(() => shader);
|
|
1417
|
+
const { Texture: Texture2 } = await Promise.resolve().then(() => texture);
|
|
1418
|
+
const { VertexBuffer: VertexBuffer2 } = await Promise.resolve().then(() => buffer);
|
|
1419
|
+
console.log(
|
|
1420
|
+
`✓ Environment detected: ${ResourceLoaderFactory.detectEnvironment()}`
|
|
1421
|
+
);
|
|
1422
|
+
const pipeline = await createResourcePipeline({
|
|
1423
|
+
concurrency: 5,
|
|
1424
|
+
cache: true,
|
|
1425
|
+
baseDir: process.cwd()
|
|
1426
|
+
});
|
|
1427
|
+
console.log("✓ Resource pipeline created for Node.js");
|
|
1428
|
+
const shaders = [
|
|
1429
|
+
{
|
|
1430
|
+
name: "basic",
|
|
1431
|
+
vertex: "resources/shaders/basic.vert",
|
|
1432
|
+
fragment: "resources/shaders/basic.frag"
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
name: "glow",
|
|
1436
|
+
vertex: "resources/shaders/glow.vert",
|
|
1437
|
+
fragment: "resources/shaders/glow.frag"
|
|
1438
|
+
}
|
|
1439
|
+
];
|
|
1440
|
+
console.log(`
|
|
1441
|
+
📝 Loading ${shaders.length} shaders from disk...`);
|
|
1442
|
+
const loadedShaders = await pipeline.loadShaders(shaders);
|
|
1443
|
+
for (const shader2 of loadedShaders) {
|
|
1444
|
+
console.log(` ✓ ${shader2.name}:`);
|
|
1445
|
+
console.log(` Vertex: ${shader2.vertex.length} chars`);
|
|
1446
|
+
console.log(` Fragment: ${shader2.fragment.length} chars`);
|
|
1447
|
+
}
|
|
1448
|
+
console.log(`
|
|
1449
|
+
💾 Cache size: ${pipeline.getCacheSize()} resources`);
|
|
1450
|
+
const gdevice2 = new GraphicsDevice2(800, 600);
|
|
1451
|
+
const gl2 = gdevice2.getGLContext();
|
|
1452
|
+
console.log(`✓ Graphics device initialized (800x600)`);
|
|
1453
|
+
const glowShader = loadedShaders.find((s) => s.name === "glow");
|
|
1454
|
+
const shader$1 = gdevice2.createShader(glowShader.vertex, glowShader.fragment);
|
|
1455
|
+
console.log("✓ Shader compiled from loaded files");
|
|
1456
|
+
const texture$1 = Texture2.createGradient(gl2, 256, 256);
|
|
1457
|
+
console.log("✓ Gradient texture created");
|
|
1458
|
+
const quadBuffer = new VertexBuffer2(
|
|
1459
|
+
gl2,
|
|
1460
|
+
GEOMETRY.quad.vertices,
|
|
1461
|
+
GEOMETRY.quad.stride
|
|
1462
|
+
);
|
|
1463
|
+
console.log(
|
|
1464
|
+
`✓ Quad buffer created (${quadBuffer.getVertexCount()} vertices)`
|
|
1465
|
+
);
|
|
1466
|
+
shader$1.use();
|
|
1467
|
+
const posAttr = shader$1.getAttributeLocation("aPosition");
|
|
1468
|
+
const texCoordAttr = shader$1.getAttributeLocation("aTexCoord");
|
|
1469
|
+
const textureUniform = shader$1.getUniformLocation("uTexture");
|
|
1470
|
+
const matrixUniform = shader$1.getUniformLocation("uMatrix");
|
|
1471
|
+
const colorUniform = shader$1.getUniformLocation("uColor");
|
|
1472
|
+
const glowIntensityUniform = shader$1.getUniformLocation("uGlowIntensity");
|
|
1473
|
+
quadBuffer.bind();
|
|
1474
|
+
gl2.enableVertexAttribArray(posAttr);
|
|
1475
|
+
gl2.vertexAttribPointer(posAttr, 3, gl2.FLOAT, false, GEOMETRY.quad.stride, 0);
|
|
1476
|
+
gl2.enableVertexAttribArray(texCoordAttr);
|
|
1477
|
+
gl2.vertexAttribPointer(
|
|
1478
|
+
texCoordAttr,
|
|
1479
|
+
2,
|
|
1480
|
+
gl2.FLOAT,
|
|
1481
|
+
false,
|
|
1482
|
+
GEOMETRY.quad.stride,
|
|
1483
|
+
3 * 4
|
|
1484
|
+
);
|
|
1485
|
+
texture$1.bind(0);
|
|
1486
|
+
gl2.uniform1i(textureUniform, 0);
|
|
1487
|
+
gdevice2.clear({ r: 0.1, g: 0.1, b: 0.1, a: 1 });
|
|
1488
|
+
const testQuads = [
|
|
1489
|
+
{ x: -0.3, y: 0.3, color: [1, 0.2, 0.2], glow: 1.5 },
|
|
1490
|
+
{ x: 0.3, y: 0.3, color: [0.2, 1, 0.2], glow: 1.8 },
|
|
1491
|
+
{ x: -0.3, y: -0.3, color: [0.2, 0.5, 1], glow: 2 },
|
|
1492
|
+
{ x: 0.3, y: -0.3, color: [1, 1, 0.2], glow: 1.6 }
|
|
1493
|
+
];
|
|
1494
|
+
for (const quad of testQuads) {
|
|
1495
|
+
const matrix = createIdentityMatrix();
|
|
1496
|
+
translateMatrix(matrix, quad.x, quad.y, 0);
|
|
1497
|
+
scaleMatrix(matrix, 0.4, 0.4, 1);
|
|
1498
|
+
if (matrixUniform) gl2.uniformMatrix4fv(matrixUniform, false, matrix);
|
|
1499
|
+
if (colorUniform)
|
|
1500
|
+
gl2.uniform3f(colorUniform, quad.color[0], quad.color[1], quad.color[2]);
|
|
1501
|
+
if (glowIntensityUniform) gl2.uniform1f(glowIntensityUniform, quad.glow);
|
|
1502
|
+
gl2.drawArrays(gl2.TRIANGLES, 0, quadBuffer.getVertexCount());
|
|
1503
|
+
}
|
|
1504
|
+
const renderingContext2 = gdevice2.getRenderingContext();
|
|
1505
|
+
const pixelData = renderingContext2.readPixels();
|
|
1506
|
+
const ppmPath = "./resource-loader-demo-output.ppm";
|
|
1507
|
+
savePPM(pixelData, 800, 600, ppmPath);
|
|
1508
|
+
console.log(`✓ Test frame rendered and saved to ${ppmPath}`);
|
|
1509
|
+
console.log("✓ Resource loader demo complete!\n");
|
|
1510
|
+
quadBuffer.dispose();
|
|
1511
|
+
texture$1.dispose();
|
|
1512
|
+
shader$1.dispose();
|
|
1513
|
+
gdevice2.dispose();
|
|
1514
|
+
}
|
|
1515
|
+
function savePPM(pixelData, width, height, outputPath) {
|
|
1516
|
+
const ppmHeader = `P6
|
|
1517
|
+
${width} ${height}
|
|
1518
|
+
255
|
|
1519
|
+
`;
|
|
1520
|
+
const ppmData = Buffer.alloc(3 * width * height);
|
|
1521
|
+
for (let i = 0; i < width * height; i++) {
|
|
1522
|
+
const srcIdx = i * 4;
|
|
1523
|
+
const dstIdx = i * 3;
|
|
1524
|
+
ppmData[dstIdx] = pixelData[srcIdx];
|
|
1525
|
+
ppmData[dstIdx + 1] = pixelData[srcIdx + 1];
|
|
1526
|
+
ppmData[dstIdx + 2] = pixelData[srcIdx + 2];
|
|
1527
|
+
}
|
|
1528
|
+
fs.writeFileSync(outputPath, ppmHeader);
|
|
1529
|
+
fs.appendFileSync(outputPath, ppmData);
|
|
1530
|
+
}
|
|
1531
|
+
function createIdentityMatrix() {
|
|
1532
|
+
return new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
1533
|
+
}
|
|
1534
|
+
function translateMatrix(mat, x, y, z) {
|
|
1535
|
+
mat[12] += x;
|
|
1536
|
+
mat[13] += y;
|
|
1537
|
+
mat[14] += z;
|
|
1538
|
+
}
|
|
1539
|
+
function scaleMatrix(mat, x, y, z) {
|
|
1540
|
+
mat[0] *= x;
|
|
1541
|
+
mat[5] *= y;
|
|
1542
|
+
mat[10] *= z;
|
|
1543
|
+
}
|
|
1544
|
+
runResourceLoaderDemo().catch((error) => {
|
|
1545
|
+
console.error("❌ Resource loader demo failed:", error);
|
|
1546
|
+
});
|
|
1547
|
+
const gdevice = new GraphicsDevice(WIDTH, HEIGHT);
|
|
1548
|
+
const gl = gdevice.getGLContext();
|
|
1549
|
+
const renderingContext = gdevice.getRenderingContext();
|
|
1550
|
+
let sdlWindow = null;
|
|
1551
|
+
try {
|
|
1552
|
+
sdlWindow = new SDLWindow(WIDTH, HEIGHT, "Bloody Engine - Live Rendering");
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
console.warn("⚠ SDL window creation failed:", error);
|
|
1555
|
+
console.log("Running in headless mode - output will be saved to file only");
|
|
1556
|
+
}
|
|
1557
|
+
console.log(`✓ Graphics device initialized (${WIDTH}x${HEIGHT})`);
|
|
1558
|
+
console.log(
|
|
1559
|
+
`✓ Environment: ${gdevice.isBrowser() ? "Browser (WebGL)" : "Node.js (headless-gl)"}`
|
|
1560
|
+
);
|
|
1561
|
+
console.log("\n✨ Setting up Projection Visualization System...");
|
|
1562
|
+
console.log("\n📊 Running projection example scenario:");
|
|
1563
|
+
runExampleScenario();
|
|
1564
|
+
const projectionEntities = PROJECTION_ENTITIES.map(
|
|
1565
|
+
(entity) => {
|
|
1566
|
+
const screenPos = gridToScreen(entity.gridPos, PROJECTION_CONFIG);
|
|
1567
|
+
return {
|
|
1568
|
+
gridPos: entity.gridPos,
|
|
1569
|
+
screenPos,
|
|
1570
|
+
color: entity.color,
|
|
1571
|
+
size: entity.size,
|
|
1572
|
+
name: entity.name
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
);
|
|
1576
|
+
console.log(
|
|
1577
|
+
`✓ ${projectionEntities.length} projection entities prepared for visualization`
|
|
1578
|
+
);
|
|
1579
|
+
console.log("\n📍 Projection Entities:");
|
|
1580
|
+
projectionEntities.forEach((entity) => {
|
|
1581
|
+
console.log(
|
|
1582
|
+
` • ${entity.name} at grid (${entity.gridPos.xgrid}, ${entity.gridPos.ygrid}, ${entity.gridPos.zheight}) → screen (${entity.screenPos.xscreen.toFixed(0)}, ${entity.screenPos.yscreen.toFixed(0)})`
|
|
1583
|
+
);
|
|
1584
|
+
});
|
|
1585
|
+
const ACTIVE_SHADER = "PSYCHEDELIC";
|
|
1586
|
+
const shaderPreset = SHADER_LIBRARY[ACTIVE_SHADER];
|
|
1587
|
+
const vertexShaderSource = shaderPreset.vertex;
|
|
1588
|
+
const fragmentShaderSource = shaderPreset.fragment;
|
|
1589
|
+
async function runSpriteBatchRendererV2Demo() {
|
|
1590
|
+
console.log("\n" + "=".repeat(60));
|
|
1591
|
+
console.log("🎨 SPRITE BATCH RENDERER V2 DEMO (2.5D Sprites)");
|
|
1592
|
+
console.log("=".repeat(60));
|
|
1593
|
+
const { SpriteBatchRenderer } = await import("./batch-renderer-JqZ4TYcL.js");
|
|
1594
|
+
const { Camera } = await import("./camera-A8EGrk7U.js");
|
|
1595
|
+
const gdevice2 = new GraphicsDevice(WIDTH, HEIGHT);
|
|
1596
|
+
const gl2 = gdevice2.getGLContext();
|
|
1597
|
+
const renderingContext2 = gdevice2.getRenderingContext();
|
|
1598
|
+
let sdlWindow2 = null;
|
|
1599
|
+
try {
|
|
1600
|
+
sdlWindow2 = new SDLWindow(WIDTH, HEIGHT, "Bloody Engine - V2 Sprites + Camera");
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
console.warn("⚠ SDL window creation failed, running in headless mode");
|
|
1603
|
+
}
|
|
1604
|
+
console.log(`✓ Graphics device initialized (${WIDTH}x${HEIGHT})`);
|
|
1605
|
+
const { SHADERS_V2: SHADERS_V22 } = await Promise.resolve().then(() => scene);
|
|
1606
|
+
const shader2 = gdevice2.createShader(SHADERS_V22.vertex, SHADERS_V22.fragment);
|
|
1607
|
+
console.log("✓ V2 Shader compiled (supports color tint and texture index)");
|
|
1608
|
+
const texture2 = Texture.createGradient(gl2, 256, 256);
|
|
1609
|
+
console.log("✓ Gradient texture created");
|
|
1610
|
+
const spriteBatchRenderer = new SpriteBatchRenderer(gl2, shader2, 1e3);
|
|
1611
|
+
spriteBatchRenderer.setTexture(texture2);
|
|
1612
|
+
console.log("✓ Sprite batch renderer created (V2)");
|
|
1613
|
+
const camera = new Camera(0, 0, 1);
|
|
1614
|
+
console.log("✓ Camera created");
|
|
1615
|
+
console.log("\n🎮 Camera Controls:");
|
|
1616
|
+
console.log(" • WASD / Arrow Keys - Move camera");
|
|
1617
|
+
console.log(" • Q / E - Zoom out / in");
|
|
1618
|
+
console.log(" • R - Reset camera");
|
|
1619
|
+
console.log(" • ESC - Exit demo");
|
|
1620
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1621
|
+
const moveSpeed = 0.5;
|
|
1622
|
+
const zoomSpeed = 2;
|
|
1623
|
+
let startTime = Date.now();
|
|
1624
|
+
let frameCount = 0;
|
|
1625
|
+
let lastTime = startTime;
|
|
1626
|
+
if (sdlWindow2) {
|
|
1627
|
+
sdlWindow2.on("keyDown", (event) => {
|
|
1628
|
+
keys.add(event.key.toLowerCase());
|
|
1629
|
+
if (event.key.toLowerCase() === "r") {
|
|
1630
|
+
camera.reset();
|
|
1631
|
+
console.log("📷 Camera reset to default position");
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
sdlWindow2.on("keyUp", (event) => {
|
|
1635
|
+
keys.delete(event.key.toLowerCase());
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
function getSpriteQuadsForFrame(elapsedSeconds) {
|
|
1639
|
+
const quads = [];
|
|
1640
|
+
const angle1 = elapsedSeconds * 2;
|
|
1641
|
+
quads.push({
|
|
1642
|
+
x: 0,
|
|
1643
|
+
y: 0,
|
|
1644
|
+
z: -0.5,
|
|
1645
|
+
width: 0.3,
|
|
1646
|
+
height: 0.3,
|
|
1647
|
+
rotation: angle1,
|
|
1648
|
+
color: { r: 1, g: 0.2, b: 0.2, a: 0.7 },
|
|
1649
|
+
texIndex: 0
|
|
1650
|
+
});
|
|
1651
|
+
const angle2 = elapsedSeconds * 1.5;
|
|
1652
|
+
const orbitRadius2 = 0.4;
|
|
1653
|
+
quads.push({
|
|
1654
|
+
x: Math.cos(angle2) * orbitRadius2,
|
|
1655
|
+
y: Math.sin(angle2) * orbitRadius2,
|
|
1656
|
+
z: 0,
|
|
1657
|
+
width: 0.25,
|
|
1658
|
+
height: 0.25,
|
|
1659
|
+
rotation: -angle2,
|
|
1660
|
+
color: { r: 0.2, g: 1, b: 0.2, a: 1 },
|
|
1661
|
+
texIndex: 0
|
|
1662
|
+
});
|
|
1663
|
+
const angle3 = elapsedSeconds * 0.8;
|
|
1664
|
+
const orbitRadius3 = 0.5;
|
|
1665
|
+
quads.push({
|
|
1666
|
+
x: Math.cos(angle3 * 2) * orbitRadius3,
|
|
1667
|
+
y: Math.sin(angle3) * orbitRadius3,
|
|
1668
|
+
z: 0.5,
|
|
1669
|
+
width: 0.2,
|
|
1670
|
+
height: 0.2,
|
|
1671
|
+
rotation: angle3 * 3,
|
|
1672
|
+
color: { r: 0.2, g: 0.5, b: 1, a: 0.9 },
|
|
1673
|
+
texIndex: 0
|
|
1674
|
+
});
|
|
1675
|
+
const bounce = 0.3 * Math.sin(elapsedSeconds * 3);
|
|
1676
|
+
quads.push({
|
|
1677
|
+
x: -0.4,
|
|
1678
|
+
y: bounce,
|
|
1679
|
+
z: 0.2,
|
|
1680
|
+
width: 0.2,
|
|
1681
|
+
height: 0.2,
|
|
1682
|
+
rotation: elapsedSeconds * 4,
|
|
1683
|
+
color: { r: 1, g: 1, b: 0.2, a: 1 },
|
|
1684
|
+
uvRect: { uMin: 0, vMin: 0, uMax: 0.5, vMax: 0.5 },
|
|
1685
|
+
texIndex: 0
|
|
1686
|
+
});
|
|
1687
|
+
const pulse = 0.15 + 0.1 * Math.sin(elapsedSeconds * 2.5);
|
|
1688
|
+
const pulseColor = 0.5 + 0.5 * Math.sin(elapsedSeconds * 3);
|
|
1689
|
+
quads.push({
|
|
1690
|
+
x: 0.4,
|
|
1691
|
+
y: 0,
|
|
1692
|
+
z: -0.2,
|
|
1693
|
+
width: pulse,
|
|
1694
|
+
height: pulse,
|
|
1695
|
+
rotation: -elapsedSeconds,
|
|
1696
|
+
color: { r: 0.2, g: pulseColor, b: pulseColor, a: 1 },
|
|
1697
|
+
texIndex: 0
|
|
1698
|
+
});
|
|
1699
|
+
const angle6 = elapsedSeconds * 1.2;
|
|
1700
|
+
const r6 = 0.35 + 0.15 * Math.sin(elapsedSeconds * 2);
|
|
1701
|
+
quads.push({
|
|
1702
|
+
x: Math.cos(angle6) * r6,
|
|
1703
|
+
y: Math.sin(angle6 * 2) * r6,
|
|
1704
|
+
z: 0.3,
|
|
1705
|
+
width: 0.18,
|
|
1706
|
+
height: 0.18,
|
|
1707
|
+
rotation: elapsedSeconds * 2.5,
|
|
1708
|
+
color: { r: 1, g: 0.2, b: 1, a: 0.8 },
|
|
1709
|
+
uvRect: { uMin: 0.5, vMin: 0.5, uMax: 1, vMax: 1 },
|
|
1710
|
+
texIndex: 1
|
|
1711
|
+
});
|
|
1712
|
+
return quads;
|
|
1713
|
+
}
|
|
1714
|
+
function renderFrame() {
|
|
1715
|
+
const now = Date.now();
|
|
1716
|
+
const deltaTime = (now - lastTime) / 1e3;
|
|
1717
|
+
const elapsedSeconds = (now - startTime) / 1e3;
|
|
1718
|
+
lastTime = now;
|
|
1719
|
+
let cameraMoved = false;
|
|
1720
|
+
const moveAmount = moveSpeed * deltaTime;
|
|
1721
|
+
if (keys.has("w") || keys.has("arrowup")) {
|
|
1722
|
+
camera.y += moveAmount;
|
|
1723
|
+
cameraMoved = true;
|
|
1724
|
+
}
|
|
1725
|
+
if (keys.has("s") || keys.has("arrowdown")) {
|
|
1726
|
+
camera.y -= moveAmount;
|
|
1727
|
+
cameraMoved = true;
|
|
1728
|
+
}
|
|
1729
|
+
if (keys.has("a") || keys.has("arrowleft")) {
|
|
1730
|
+
camera.x -= moveAmount;
|
|
1731
|
+
cameraMoved = true;
|
|
1732
|
+
}
|
|
1733
|
+
if (keys.has("d") || keys.has("arrowright")) {
|
|
1734
|
+
camera.x += moveAmount;
|
|
1735
|
+
cameraMoved = true;
|
|
1736
|
+
}
|
|
1737
|
+
const zoomFactor = 1 + (zoomSpeed - 1) * deltaTime;
|
|
1738
|
+
if (keys.has("q")) {
|
|
1739
|
+
camera.zoomBy(1 / zoomFactor);
|
|
1740
|
+
cameraMoved = true;
|
|
1741
|
+
}
|
|
1742
|
+
if (keys.has("e")) {
|
|
1743
|
+
camera.zoomBy(zoomFactor);
|
|
1744
|
+
cameraMoved = true;
|
|
1745
|
+
}
|
|
1746
|
+
if (cameraMoved && frameCount % 30 === 0) {
|
|
1747
|
+
console.log(
|
|
1748
|
+
`📷 Camera: x=${camera.x.toFixed(2)}, y=${camera.y.toFixed(2)}, zoom=${camera.zoom.toFixed(2)}x`
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
const quads = getSpriteQuadsForFrame(elapsedSeconds);
|
|
1752
|
+
spriteBatchRenderer.clear();
|
|
1753
|
+
for (const quad of quads) {
|
|
1754
|
+
spriteBatchRenderer.addQuad(quad);
|
|
1755
|
+
}
|
|
1756
|
+
gdevice2.clear({ r: 0.1, g: 0.1, b: 0.12, a: 1 });
|
|
1757
|
+
spriteBatchRenderer.render(camera);
|
|
1758
|
+
gdevice2.present();
|
|
1759
|
+
if (sdlWindow2 && sdlWindow2.isOpen()) {
|
|
1760
|
+
const pixelData = renderingContext2.readPixels();
|
|
1761
|
+
sdlWindow2.updatePixels(pixelData);
|
|
1762
|
+
}
|
|
1763
|
+
frameCount++;
|
|
1764
|
+
}
|
|
1765
|
+
console.log("\n🎬 Starting render loop (press ESC to exit)...");
|
|
1766
|
+
const frames = [];
|
|
1767
|
+
const renderStart = Date.now();
|
|
1768
|
+
let running = true;
|
|
1769
|
+
if (sdlWindow2) {
|
|
1770
|
+
sdlWindow2.on("close", () => {
|
|
1771
|
+
running = false;
|
|
1772
|
+
});
|
|
1773
|
+
sdlWindow2.on("keyDown", (event) => {
|
|
1774
|
+
if (event.key === "escape") {
|
|
1775
|
+
running = false;
|
|
1776
|
+
console.log("\n✓ Exiting demo...");
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
while (running && (!sdlWindow2 || sdlWindow2.isOpen())) {
|
|
1781
|
+
renderFrame();
|
|
1782
|
+
if (frameCount === 1) {
|
|
1783
|
+
const pixelData = renderingContext2.readPixels();
|
|
1784
|
+
frames.push(Buffer.from(pixelData));
|
|
1785
|
+
}
|
|
1786
|
+
await new Promise((resolve) => setTimeout(resolve, 16));
|
|
1787
|
+
}
|
|
1788
|
+
const renderEnd = Date.now();
|
|
1789
|
+
const renderTime = (renderEnd - renderStart) / 1e3;
|
|
1790
|
+
const avgFPS = frameCount / renderTime;
|
|
1791
|
+
console.log(`✓ Rendered ${frameCount} frames in ${renderTime.toFixed(2)}s`);
|
|
1792
|
+
console.log(`✓ Average FPS: ${avgFPS.toFixed(1)}`);
|
|
1793
|
+
console.log(
|
|
1794
|
+
`✓ Sprite batch renderer tested with ${spriteBatchRenderer.getQuadCount()} sprites`
|
|
1795
|
+
);
|
|
1796
|
+
if (frames.length > 0) {
|
|
1797
|
+
const pixelData = frames[0];
|
|
1798
|
+
const ppmHeader = `P6
|
|
1799
|
+
${WIDTH} ${HEIGHT}
|
|
1800
|
+
255
|
|
1801
|
+
`;
|
|
1802
|
+
const ppmData = Buffer.alloc(3 * WIDTH * HEIGHT);
|
|
1803
|
+
for (let i = 0; i < WIDTH * HEIGHT; i++) {
|
|
1804
|
+
const srcIdx = i * 4;
|
|
1805
|
+
const dstIdx = i * 3;
|
|
1806
|
+
ppmData[dstIdx] = pixelData[srcIdx];
|
|
1807
|
+
ppmData[dstIdx + 1] = pixelData[srcIdx + 1];
|
|
1808
|
+
ppmData[dstIdx + 2] = pixelData[srcIdx + 2];
|
|
1809
|
+
}
|
|
1810
|
+
const ppmPath = "./sprite-batch-renderer-v2-demo.ppm";
|
|
1811
|
+
fs.writeFileSync(ppmPath, ppmHeader);
|
|
1812
|
+
fs.appendFileSync(ppmPath, ppmData);
|
|
1813
|
+
console.log(`✓ First frame saved to ${ppmPath}`);
|
|
1814
|
+
}
|
|
1815
|
+
spriteBatchRenderer.dispose();
|
|
1816
|
+
gdevice2.dispose();
|
|
1817
|
+
if (sdlWindow2) {
|
|
1818
|
+
sdlWindow2.cleanup();
|
|
1819
|
+
}
|
|
1820
|
+
console.log("✓ Sprite batch renderer demo complete!\n");
|
|
1821
|
+
}
|
|
1822
|
+
(async () => {
|
|
1823
|
+
try {
|
|
1824
|
+
console.log("\n--- Setting up Textured Quad ---");
|
|
1825
|
+
const shader2 = gdevice.createShader(
|
|
1826
|
+
vertexShaderSource,
|
|
1827
|
+
fragmentShaderSource
|
|
1828
|
+
);
|
|
1829
|
+
console.log(`✓ Shader compiled and linked (${ACTIVE_SHADER})`);
|
|
1830
|
+
const quadVertices = GEOMETRY.quad.vertices;
|
|
1831
|
+
const vertexBuffer = new VertexBuffer(
|
|
1832
|
+
gl,
|
|
1833
|
+
quadVertices,
|
|
1834
|
+
GEOMETRY.quad.stride
|
|
1835
|
+
);
|
|
1836
|
+
console.log(
|
|
1837
|
+
`✓ Vertex buffer created (${vertexBuffer.getVertexCount()} vertices)`
|
|
1838
|
+
);
|
|
1839
|
+
const triangleVertices = GEOMETRY.triangle.vertices;
|
|
1840
|
+
const triangleBuffer = new VertexBuffer(
|
|
1841
|
+
gl,
|
|
1842
|
+
triangleVertices,
|
|
1843
|
+
GEOMETRY.triangle.stride
|
|
1844
|
+
);
|
|
1845
|
+
console.log(
|
|
1846
|
+
`✓ Triangle buffer created (${triangleBuffer.getVertexCount()} vertices)`
|
|
1847
|
+
);
|
|
1848
|
+
const texture2 = Texture.createGradient(
|
|
1849
|
+
gl,
|
|
1850
|
+
TEXTURE_CONFIG.size,
|
|
1851
|
+
TEXTURE_CONFIG.size
|
|
1852
|
+
);
|
|
1853
|
+
console.log("✓ Gradient texture created (256x256)");
|
|
1854
|
+
const posAttr = shader2.getAttributeLocation("aPosition");
|
|
1855
|
+
const texCoordAttr = shader2.getAttributeLocation("aTexCoord");
|
|
1856
|
+
const textureUniform = shader2.getUniformLocation("uTexture");
|
|
1857
|
+
const matrixUniform = shader2.getUniformLocation("uMatrix");
|
|
1858
|
+
const colorUniform = shader2.getUniformLocation("uColor");
|
|
1859
|
+
const glowIntensityUniform = shader2.getUniformLocation("uGlowIntensity");
|
|
1860
|
+
const timeUniform = shader2.getUniformLocation("uTime");
|
|
1861
|
+
console.log(
|
|
1862
|
+
`✓ Attributes located (position=${posAttr}, texCoord=${texCoordAttr})`
|
|
1863
|
+
);
|
|
1864
|
+
shader2.use();
|
|
1865
|
+
vertexBuffer.bind();
|
|
1866
|
+
gl.enableVertexAttribArray(posAttr);
|
|
1867
|
+
gl.vertexAttribPointer(posAttr, 3, gl.FLOAT, false, 5 * 4, 0);
|
|
1868
|
+
gl.enableVertexAttribArray(texCoordAttr);
|
|
1869
|
+
gl.vertexAttribPointer(texCoordAttr, 2, gl.FLOAT, false, 5 * 4, 3 * 4);
|
|
1870
|
+
console.log("✓ Vertex attributes configured");
|
|
1871
|
+
texture2.bind(0);
|
|
1872
|
+
gl.uniform1i(textureUniform, 0);
|
|
1873
|
+
console.log("✓ Texture bound to unit 0");
|
|
1874
|
+
const identityMatrix = new Float32Array([
|
|
1875
|
+
1,
|
|
1876
|
+
0,
|
|
1877
|
+
0,
|
|
1878
|
+
0,
|
|
1879
|
+
0,
|
|
1880
|
+
1,
|
|
1881
|
+
0,
|
|
1882
|
+
0,
|
|
1883
|
+
0,
|
|
1884
|
+
0,
|
|
1885
|
+
1,
|
|
1886
|
+
0,
|
|
1887
|
+
0,
|
|
1888
|
+
0,
|
|
1889
|
+
0,
|
|
1890
|
+
1
|
|
1891
|
+
]);
|
|
1892
|
+
if (matrixUniform) {
|
|
1893
|
+
gl.uniformMatrix4fv(matrixUniform, false, identityMatrix);
|
|
1894
|
+
}
|
|
1895
|
+
gdevice.clear({ r: 0.2, g: 0.2, b: 0.2, a: 1 });
|
|
1896
|
+
console.log("✓ Screen cleared");
|
|
1897
|
+
gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.getVertexCount());
|
|
1898
|
+
console.log(`✓ Rendered ${vertexBuffer.getVertexCount()} vertices`);
|
|
1899
|
+
gdevice.present();
|
|
1900
|
+
console.log("✓ Frame presented");
|
|
1901
|
+
const pixelData = renderingContext.readPixels();
|
|
1902
|
+
console.log(
|
|
1903
|
+
`✓ Captured frame (${WIDTH}x${HEIGHT}, ${pixelData.length} bytes)`
|
|
1904
|
+
);
|
|
1905
|
+
if (sdlWindow && sdlWindow.isOpen()) {
|
|
1906
|
+
sdlWindow.updatePixels(pixelData);
|
|
1907
|
+
}
|
|
1908
|
+
const ppmHeader = `P6
|
|
1909
|
+
${WIDTH} ${HEIGHT}
|
|
1910
|
+
255
|
|
1911
|
+
`;
|
|
1912
|
+
const ppmData = Buffer.alloc(3 * WIDTH * HEIGHT);
|
|
1913
|
+
for (let i = 0; i < WIDTH * HEIGHT; i++) {
|
|
1914
|
+
const srcIdx = i * 4;
|
|
1915
|
+
const dstIdx = i * 3;
|
|
1916
|
+
ppmData[dstIdx] = pixelData[srcIdx];
|
|
1917
|
+
ppmData[dstIdx + 1] = pixelData[srcIdx + 1];
|
|
1918
|
+
ppmData[dstIdx + 2] = pixelData[srcIdx + 2];
|
|
1919
|
+
}
|
|
1920
|
+
const ppmPath = "./rendered-textured-quad.ppm";
|
|
1921
|
+
fs.writeFileSync(ppmPath, ppmHeader);
|
|
1922
|
+
fs.appendFileSync(ppmPath, ppmData);
|
|
1923
|
+
console.log(`✓ Frame saved to ${ppmPath}`);
|
|
1924
|
+
if (!sdlWindow) {
|
|
1925
|
+
try {
|
|
1926
|
+
const ppmAbsPath = path__default.resolve(ppmPath);
|
|
1927
|
+
if (process.platform === "win32") {
|
|
1928
|
+
execSync(`start "" "${ppmAbsPath}"`);
|
|
1929
|
+
} else if (process.platform === "darwin") {
|
|
1930
|
+
execSync(`open "${ppmAbsPath}"`);
|
|
1931
|
+
} else {
|
|
1932
|
+
execSync(`xdg-open "${ppmAbsPath}"`);
|
|
1933
|
+
}
|
|
1934
|
+
console.log("✓ Image opened in default viewer");
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
console.warn(
|
|
1937
|
+
"⚠ Could not auto-open image in viewer (no default handler)"
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
console.log(`
|
|
1942
|
+
✓ Sample pixels (RGBA):`);
|
|
1943
|
+
for (let i = 0; i < 4; i++) {
|
|
1944
|
+
const offset = i * 4;
|
|
1945
|
+
console.log(
|
|
1946
|
+
` Pixel ${i}: R=${pixelData[offset]}, G=${pixelData[offset + 1]}, B=${pixelData[offset + 2]}, A=${pixelData[offset + 3]}`
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
if (sdlWindow && sdlWindow.isOpen()) {
|
|
1950
|
+
console.log("\n✓ SDL Window open - press ESC or close window to exit");
|
|
1951
|
+
console.log("💡 Window is interactive and rendering at 60 FPS");
|
|
1952
|
+
let frameCount = 0;
|
|
1953
|
+
const startTime = Date.now();
|
|
1954
|
+
const targetFPS = SCENE_CONFIG.targetFPS;
|
|
1955
|
+
const frameTimeMs = 1e3 / targetFPS;
|
|
1956
|
+
let running = true;
|
|
1957
|
+
sdlWindow.on("close", () => {
|
|
1958
|
+
running = false;
|
|
1959
|
+
});
|
|
1960
|
+
sdlWindow.on("keyDown", (event) => {
|
|
1961
|
+
if (event.key === "escape") {
|
|
1962
|
+
running = false;
|
|
1963
|
+
sdlWindow.destroy();
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
const renderLoop = () => {
|
|
1967
|
+
if (!running || !sdlWindow.isOpen()) {
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const frameStartTime = Date.now();
|
|
1971
|
+
const elapsedSeconds = frameCount * frameTimeMs / 1e3;
|
|
1972
|
+
const bgColor = getBackgroundColor(elapsedSeconds);
|
|
1973
|
+
gdevice.clear(bgColor);
|
|
1974
|
+
const quadTransforms = getQuadTransforms(elapsedSeconds);
|
|
1975
|
+
for (const transform of quadTransforms) {
|
|
1976
|
+
const matrixUniform2 = shader2.getUniformLocation("uMatrix");
|
|
1977
|
+
if (matrixUniform2) {
|
|
1978
|
+
gl.uniformMatrix4fv(matrixUniform2, false, transform.matrix);
|
|
1979
|
+
}
|
|
1980
|
+
const colorUniform2 = shader2.getUniformLocation("uColor");
|
|
1981
|
+
if (colorUniform2) {
|
|
1982
|
+
gl.uniform3f(
|
|
1983
|
+
colorUniform2,
|
|
1984
|
+
transform.color[0],
|
|
1985
|
+
transform.color[1],
|
|
1986
|
+
transform.color[2]
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
if (glowIntensityUniform) {
|
|
1990
|
+
gl.uniform1f(
|
|
1991
|
+
glowIntensityUniform,
|
|
1992
|
+
1.5 + Math.sin(elapsedSeconds) * 0.5
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
if (timeUniform) {
|
|
1996
|
+
gl.uniform1f(timeUniform, elapsedSeconds);
|
|
1997
|
+
}
|
|
1998
|
+
gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.getVertexCount());
|
|
1999
|
+
}
|
|
2000
|
+
const triangleTransforms = getTriangleTransforms(elapsedSeconds);
|
|
2001
|
+
for (const transform of triangleTransforms) {
|
|
2002
|
+
const matrixUniform2 = shader2.getUniformLocation("uMatrix");
|
|
2003
|
+
if (matrixUniform2) {
|
|
2004
|
+
gl.uniformMatrix4fv(matrixUniform2, false, transform.matrix);
|
|
2005
|
+
}
|
|
2006
|
+
const colorUniform2 = shader2.getUniformLocation("uColor");
|
|
2007
|
+
if (colorUniform2) {
|
|
2008
|
+
gl.uniform3f(
|
|
2009
|
+
colorUniform2,
|
|
2010
|
+
transform.color[0],
|
|
2011
|
+
transform.color[1],
|
|
2012
|
+
transform.color[2]
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
if (glowIntensityUniform) {
|
|
2016
|
+
gl.uniform1f(
|
|
2017
|
+
glowIntensityUniform,
|
|
2018
|
+
2 + Math.cos(elapsedSeconds * 0.7) * 0.8
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
if (timeUniform) {
|
|
2022
|
+
gl.uniform1f(timeUniform, elapsedSeconds);
|
|
2023
|
+
}
|
|
2024
|
+
gl.drawArrays(gl.TRIANGLES, 0, triangleBuffer.getVertexCount());
|
|
2025
|
+
}
|
|
2026
|
+
for (const entity of projectionEntities) {
|
|
2027
|
+
const screenX = entity.screenPos.xscreen / WIDTH * 2 - 1;
|
|
2028
|
+
const screenY = 1 - entity.screenPos.yscreen / HEIGHT * 2;
|
|
2029
|
+
const scale = entity.size * 0.1;
|
|
2030
|
+
const entityMatrix = new Float32Array([
|
|
2031
|
+
scale,
|
|
2032
|
+
0,
|
|
2033
|
+
0,
|
|
2034
|
+
0,
|
|
2035
|
+
0,
|
|
2036
|
+
scale,
|
|
2037
|
+
0,
|
|
2038
|
+
0,
|
|
2039
|
+
0,
|
|
2040
|
+
0,
|
|
2041
|
+
1,
|
|
2042
|
+
0,
|
|
2043
|
+
screenX,
|
|
2044
|
+
screenY,
|
|
2045
|
+
0,
|
|
2046
|
+
1
|
|
2047
|
+
]);
|
|
2048
|
+
const matrixUniform2 = shader2.getUniformLocation("uMatrix");
|
|
2049
|
+
if (matrixUniform2) {
|
|
2050
|
+
gl.uniformMatrix4fv(matrixUniform2, false, entityMatrix);
|
|
2051
|
+
}
|
|
2052
|
+
const colorUniform2 = shader2.getUniformLocation("uColor");
|
|
2053
|
+
if (colorUniform2) {
|
|
2054
|
+
gl.uniform3f(
|
|
2055
|
+
colorUniform2,
|
|
2056
|
+
entity.color[0],
|
|
2057
|
+
entity.color[1],
|
|
2058
|
+
entity.color[2]
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.getVertexCount());
|
|
2062
|
+
}
|
|
2063
|
+
gdevice.present();
|
|
2064
|
+
const framePixels = renderingContext.readPixels();
|
|
2065
|
+
sdlWindow.updatePixels(framePixels);
|
|
2066
|
+
frameCount++;
|
|
2067
|
+
if (frameCount % 60 === 0) {
|
|
2068
|
+
const totalElapsed = Date.now() - startTime;
|
|
2069
|
+
const fps = frameCount / totalElapsed * 1e3;
|
|
2070
|
+
console.log(`FPS: ${fps.toFixed(1)} | Frames: ${frameCount}`);
|
|
2071
|
+
}
|
|
2072
|
+
const elapsed = Date.now() - frameStartTime;
|
|
2073
|
+
const sleepTime = frameTimeMs - elapsed;
|
|
2074
|
+
if (sleepTime > 0) {
|
|
2075
|
+
setTimeout(renderLoop, sleepTime);
|
|
2076
|
+
} else {
|
|
2077
|
+
setImmediate(renderLoop);
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
renderLoop();
|
|
2081
|
+
await new Promise((resolve) => {
|
|
2082
|
+
const checkInterval = setInterval(() => {
|
|
2083
|
+
if (!sdlWindow.isOpen() || !running) {
|
|
2084
|
+
clearInterval(checkInterval);
|
|
2085
|
+
resolve(null);
|
|
2086
|
+
}
|
|
2087
|
+
}, 100);
|
|
2088
|
+
});
|
|
2089
|
+
const totalTime = (Date.now() - startTime) / 1e3;
|
|
2090
|
+
console.log(
|
|
2091
|
+
`✓ Rendered ${frameCount} frames in ${totalTime.toFixed(2)}s`
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
vertexBuffer.dispose();
|
|
2095
|
+
texture2.dispose();
|
|
2096
|
+
shader2.dispose();
|
|
2097
|
+
console.log("\n✓ Resources cleaned up");
|
|
2098
|
+
if (sdlWindow) {
|
|
2099
|
+
sdlWindow.cleanup();
|
|
2100
|
+
}
|
|
2101
|
+
gdevice.dispose();
|
|
2102
|
+
console.log("✓ Graphics device disposed");
|
|
2103
|
+
console.log("\n=== Rendering Complete ===\n");
|
|
2104
|
+
await runSpriteBatchRendererV2Demo();
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
console.error("✗ Rendering failed:", error);
|
|
2107
|
+
if (sdlWindow) {
|
|
2108
|
+
try {
|
|
2109
|
+
sdlWindow.cleanup();
|
|
2110
|
+
} catch (e) {
|
|
2111
|
+
console.error("Error cleaning up SDL window:", e);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
gdevice.dispose();
|
|
2115
|
+
process.exit(1);
|
|
2116
|
+
}
|
|
2117
|
+
})();
|