bloody-engine 1.0.4 → 1.0.5
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/dist/node/index.js +400 -0
- package/dist/web/index.js +760 -474
- package/dist/web/index.umd.js +23 -23
- package/dist/web/public-api.d.ts +6 -1
- package/dist/web/public-api.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/web/batch-renderer.test.d.ts +0 -12
- package/dist/web/batch-renderer.test.d.ts.map +0 -1
- package/dist/web/index-node-batch.d.ts +0 -10
- package/dist/web/index-node-batch.d.ts.map +0 -1
package/dist/node/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import createGL from "gl";
|
|
2
|
+
import sdl from "@kmamal/sdl";
|
|
2
3
|
import * as fs from "fs/promises";
|
|
3
4
|
import * as path from "path";
|
|
4
5
|
class BrowserRenderingContext {
|
|
@@ -365,6 +366,99 @@ class GraphicsDevice {
|
|
|
365
366
|
);
|
|
366
367
|
}
|
|
367
368
|
}
|
|
369
|
+
class SDLWindow {
|
|
370
|
+
constructor(width, height, title = "Bloody Engine") {
|
|
371
|
+
this.closed = false;
|
|
372
|
+
this.width = width;
|
|
373
|
+
this.height = height;
|
|
374
|
+
this.title = title;
|
|
375
|
+
try {
|
|
376
|
+
this.window = sdl.video.createWindow({
|
|
377
|
+
width: this.width,
|
|
378
|
+
height: this.height,
|
|
379
|
+
title: this.title
|
|
380
|
+
});
|
|
381
|
+
if (!this.window) {
|
|
382
|
+
throw new Error("Failed to create SDL window");
|
|
383
|
+
}
|
|
384
|
+
this.window.on("close", () => {
|
|
385
|
+
this.closed = true;
|
|
386
|
+
});
|
|
387
|
+
console.log(`✓ SDL Window created (${width}x${height}): "${title}"`);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
this.cleanup();
|
|
390
|
+
throw new Error(`Window creation failed: ${error}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get window dimensions
|
|
395
|
+
*/
|
|
396
|
+
getDimensions() {
|
|
397
|
+
return { width: this.width, height: this.height };
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Display pixel data in the window
|
|
401
|
+
* @param pixels Uint8Array of RGBA pixel data
|
|
402
|
+
*/
|
|
403
|
+
updatePixels(pixels) {
|
|
404
|
+
if (!this.window || this.closed) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const buffer2 = Buffer.from(pixels);
|
|
409
|
+
const stride = this.width * 4;
|
|
410
|
+
this.window.render(this.width, this.height, stride, "rgba32", buffer2);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error("Failed to update pixels:", error);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Register an event handler
|
|
417
|
+
*/
|
|
418
|
+
on(eventName, handler) {
|
|
419
|
+
if (!this.window || this.closed) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
this.window.on(eventName, (event) => {
|
|
424
|
+
try {
|
|
425
|
+
handler(event);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error(`Error in ${eventName} handler:`, error);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
} catch (error) {
|
|
431
|
+
console.error(`Error registering ${eventName} handler:`, error);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Check if window is still open
|
|
436
|
+
*/
|
|
437
|
+
isOpen() {
|
|
438
|
+
return this.window !== null && !this.closed;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Cleanup and close window
|
|
442
|
+
*/
|
|
443
|
+
cleanup() {
|
|
444
|
+
if (this.window && !this.closed) {
|
|
445
|
+
try {
|
|
446
|
+
this.window.destroy();
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.warn("Error destroying window:", error);
|
|
449
|
+
}
|
|
450
|
+
this.window = null;
|
|
451
|
+
this.closed = true;
|
|
452
|
+
}
|
|
453
|
+
console.log("✓ SDL Window cleaned up");
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Destroy the window (alias for cleanup)
|
|
457
|
+
*/
|
|
458
|
+
destroy() {
|
|
459
|
+
this.cleanup();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
368
462
|
class Texture {
|
|
369
463
|
/**
|
|
370
464
|
* Create a texture from pixel data
|
|
@@ -1132,6 +1226,301 @@ class SpriteBatchRenderer {
|
|
|
1132
1226
|
}
|
|
1133
1227
|
}
|
|
1134
1228
|
}
|
|
1229
|
+
class GPUBasedSpriteBatchRenderer {
|
|
1230
|
+
/**
|
|
1231
|
+
* Create a new GPU-based sprite batch renderer (V3)
|
|
1232
|
+
* @param gl WebGL rendering context
|
|
1233
|
+
* @param shader Shader program to use (should be SHADERS_V3)
|
|
1234
|
+
* @param maxQuads Maximum number of quads to batch (default 1000)
|
|
1235
|
+
* @param tileSize Tile size for isometric projection (default {width: 64, height: 32})
|
|
1236
|
+
* @param zScale Scale factor for Z height (default 1.0)
|
|
1237
|
+
*/
|
|
1238
|
+
constructor(gl, shader, maxQuads = 1e3, tileSize = { width: 64, height: 32 }, zScale = 1) {
|
|
1239
|
+
this.vertexBuffer = null;
|
|
1240
|
+
this.quads = [];
|
|
1241
|
+
this.isDirty = false;
|
|
1242
|
+
this.verticesPerQuad = 6;
|
|
1243
|
+
this.floatsPerVertex = 12;
|
|
1244
|
+
this.texture = null;
|
|
1245
|
+
this.depthTestEnabled = true;
|
|
1246
|
+
this.gl = gl;
|
|
1247
|
+
this.shader = shader;
|
|
1248
|
+
this.maxQuads = maxQuads;
|
|
1249
|
+
this.tileSize = tileSize;
|
|
1250
|
+
this.zScale = zScale;
|
|
1251
|
+
this.resolution = { width: gl.canvas.width, height: gl.canvas.height };
|
|
1252
|
+
const totalFloats = maxQuads * this.verticesPerQuad * this.floatsPerVertex;
|
|
1253
|
+
this.vertexData = new Float32Array(totalFloats);
|
|
1254
|
+
const buf = gl.createBuffer();
|
|
1255
|
+
if (!buf) {
|
|
1256
|
+
throw new Error("Failed to create vertex buffer");
|
|
1257
|
+
}
|
|
1258
|
+
this.vertexBuffer = buf;
|
|
1259
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
1260
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.vertexData.byteLength, gl.DYNAMIC_DRAW);
|
|
1261
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Set the texture for batch rendering
|
|
1265
|
+
* @param texture The texture to use when rendering
|
|
1266
|
+
*/
|
|
1267
|
+
setTexture(texture) {
|
|
1268
|
+
this.texture = texture;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Add a sprite quad to the batch
|
|
1272
|
+
* If gridX and gridY are provided, uses GPU transformation.
|
|
1273
|
+
* Otherwise, converts x, y to grid coordinates.
|
|
1274
|
+
* @param quad Sprite quad instance to add
|
|
1275
|
+
*/
|
|
1276
|
+
addQuad(quad) {
|
|
1277
|
+
if (this.quads.length >= this.maxQuads) {
|
|
1278
|
+
console.warn(`Sprite batch renderer at max capacity (${this.maxQuads})`);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
this.quads.push(quad);
|
|
1282
|
+
this.isDirty = true;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Clear all quads from the batch
|
|
1286
|
+
*/
|
|
1287
|
+
clear() {
|
|
1288
|
+
this.quads = [];
|
|
1289
|
+
this.isDirty = true;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Get number of quads currently in batch
|
|
1293
|
+
*/
|
|
1294
|
+
getQuadCount() {
|
|
1295
|
+
return this.quads.length;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Update the batch - rebuilds vertex buffer if quads changed
|
|
1299
|
+
*/
|
|
1300
|
+
update() {
|
|
1301
|
+
if (!this.isDirty || this.quads.length === 0) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
let vertexIndex = 0;
|
|
1305
|
+
for (const quad of this.quads) {
|
|
1306
|
+
const {
|
|
1307
|
+
x,
|
|
1308
|
+
y,
|
|
1309
|
+
z = 0,
|
|
1310
|
+
width,
|
|
1311
|
+
height,
|
|
1312
|
+
color = { r: 1, g: 1, b: 1, a: 1 },
|
|
1313
|
+
uvRect = { uMin: 0, vMin: 0, uMax: 1, vMax: 1 },
|
|
1314
|
+
texIndex = 0,
|
|
1315
|
+
gridX,
|
|
1316
|
+
gridY
|
|
1317
|
+
} = quad;
|
|
1318
|
+
let gx, gy;
|
|
1319
|
+
if (gridX !== void 0 && gridY !== void 0) {
|
|
1320
|
+
gx = gridX;
|
|
1321
|
+
gy = gridY;
|
|
1322
|
+
} else {
|
|
1323
|
+
gx = (x / (this.tileSize.width * 0.5) + y / (this.tileSize.height * 0.5)) * 0.5;
|
|
1324
|
+
gy = (y / (this.tileSize.height * 0.5) - x / (this.tileSize.width * 0.5)) * 0.5;
|
|
1325
|
+
}
|
|
1326
|
+
const halfW = width / 2;
|
|
1327
|
+
const halfH = height / 2;
|
|
1328
|
+
const corners = [
|
|
1329
|
+
[-halfW, -halfH],
|
|
1330
|
+
// bottom-left
|
|
1331
|
+
[halfW, -halfH],
|
|
1332
|
+
// bottom-right
|
|
1333
|
+
[halfW, halfH],
|
|
1334
|
+
// top-right
|
|
1335
|
+
[halfW, halfH],
|
|
1336
|
+
// top-right (duplicate)
|
|
1337
|
+
[-halfH, halfH],
|
|
1338
|
+
// top-left
|
|
1339
|
+
[-halfW, -halfH]
|
|
1340
|
+
// bottom-left (duplicate)
|
|
1341
|
+
];
|
|
1342
|
+
const texCoords = [
|
|
1343
|
+
[uvRect.uMin, uvRect.vMin],
|
|
1344
|
+
[uvRect.uMax, uvRect.vMin],
|
|
1345
|
+
[uvRect.uMax, uvRect.vMax],
|
|
1346
|
+
[uvRect.uMax, uvRect.vMax],
|
|
1347
|
+
[uvRect.uMin, uvRect.vMax],
|
|
1348
|
+
[uvRect.uMin, uvRect.vMin]
|
|
1349
|
+
];
|
|
1350
|
+
for (let i = 0; i < corners.length; i++) {
|
|
1351
|
+
const [localX, localY] = corners[i];
|
|
1352
|
+
const [u, v] = texCoords[i];
|
|
1353
|
+
this.vertexData[vertexIndex++] = gx;
|
|
1354
|
+
this.vertexData[vertexIndex++] = gy;
|
|
1355
|
+
this.vertexData[vertexIndex++] = z;
|
|
1356
|
+
this.vertexData[vertexIndex++] = localX;
|
|
1357
|
+
this.vertexData[vertexIndex++] = localY;
|
|
1358
|
+
this.vertexData[vertexIndex++] = u;
|
|
1359
|
+
this.vertexData[vertexIndex++] = v;
|
|
1360
|
+
this.vertexData[vertexIndex++] = color.r;
|
|
1361
|
+
this.vertexData[vertexIndex++] = color.g;
|
|
1362
|
+
this.vertexData[vertexIndex++] = color.b;
|
|
1363
|
+
this.vertexData[vertexIndex++] = color.a;
|
|
1364
|
+
this.vertexData[vertexIndex++] = texIndex;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (this.vertexBuffer) {
|
|
1368
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
1369
|
+
this.gl.bufferSubData(
|
|
1370
|
+
this.gl.ARRAY_BUFFER,
|
|
1371
|
+
0,
|
|
1372
|
+
this.vertexData.subarray(0, vertexIndex)
|
|
1373
|
+
);
|
|
1374
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
|
|
1375
|
+
}
|
|
1376
|
+
this.isDirty = false;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Set whether depth testing is enabled
|
|
1380
|
+
* @param enabled Whether to enable depth testing (default true)
|
|
1381
|
+
*/
|
|
1382
|
+
setDepthTestEnabled(enabled) {
|
|
1383
|
+
this.depthTestEnabled = enabled;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Render the batch with GPU-based transformation
|
|
1387
|
+
* @param camera Camera for view transform
|
|
1388
|
+
*/
|
|
1389
|
+
render(camera) {
|
|
1390
|
+
if (this.quads.length === 0) {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
this.update();
|
|
1394
|
+
this.shader.use();
|
|
1395
|
+
if (this.depthTestEnabled) {
|
|
1396
|
+
this.gl.enable(this.gl.DEPTH_TEST);
|
|
1397
|
+
this.gl.depthFunc(this.gl.LEQUAL);
|
|
1398
|
+
} else {
|
|
1399
|
+
this.gl.disable(this.gl.DEPTH_TEST);
|
|
1400
|
+
}
|
|
1401
|
+
if (this.vertexBuffer) {
|
|
1402
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
1403
|
+
const stride = this.floatsPerVertex * 4;
|
|
1404
|
+
const attrs = {
|
|
1405
|
+
gridPosition: this.shader.getAttributeLocation("aGridPosition"),
|
|
1406
|
+
zPosition: this.shader.getAttributeLocation("aZPosition"),
|
|
1407
|
+
localOffset: this.shader.getAttributeLocation("aLocalOffset"),
|
|
1408
|
+
texCoord: this.shader.getAttributeLocation("aTexCoord"),
|
|
1409
|
+
color: this.shader.getAttributeLocation("aColor"),
|
|
1410
|
+
texIndex: this.shader.getAttributeLocation("aTexIndex")
|
|
1411
|
+
};
|
|
1412
|
+
if (attrs.gridPosition !== -1) {
|
|
1413
|
+
this.gl.enableVertexAttribArray(attrs.gridPosition);
|
|
1414
|
+
this.gl.vertexAttribPointer(
|
|
1415
|
+
attrs.gridPosition,
|
|
1416
|
+
2,
|
|
1417
|
+
this.gl.FLOAT,
|
|
1418
|
+
false,
|
|
1419
|
+
stride,
|
|
1420
|
+
0
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
if (attrs.zPosition !== -1) {
|
|
1424
|
+
this.gl.enableVertexAttribArray(attrs.zPosition);
|
|
1425
|
+
this.gl.vertexAttribPointer(
|
|
1426
|
+
attrs.zPosition,
|
|
1427
|
+
1,
|
|
1428
|
+
this.gl.FLOAT,
|
|
1429
|
+
false,
|
|
1430
|
+
stride,
|
|
1431
|
+
2 * 4
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
if (attrs.localOffset !== -1) {
|
|
1435
|
+
this.gl.enableVertexAttribArray(attrs.localOffset);
|
|
1436
|
+
this.gl.vertexAttribPointer(
|
|
1437
|
+
attrs.localOffset,
|
|
1438
|
+
2,
|
|
1439
|
+
this.gl.FLOAT,
|
|
1440
|
+
false,
|
|
1441
|
+
stride,
|
|
1442
|
+
3 * 4
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
if (attrs.texCoord !== -1) {
|
|
1446
|
+
this.gl.enableVertexAttribArray(attrs.texCoord);
|
|
1447
|
+
this.gl.vertexAttribPointer(
|
|
1448
|
+
attrs.texCoord,
|
|
1449
|
+
2,
|
|
1450
|
+
this.gl.FLOAT,
|
|
1451
|
+
false,
|
|
1452
|
+
stride,
|
|
1453
|
+
5 * 4
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
if (attrs.color !== -1) {
|
|
1457
|
+
this.gl.enableVertexAttribArray(attrs.color);
|
|
1458
|
+
this.gl.vertexAttribPointer(
|
|
1459
|
+
attrs.color,
|
|
1460
|
+
4,
|
|
1461
|
+
this.gl.FLOAT,
|
|
1462
|
+
false,
|
|
1463
|
+
stride,
|
|
1464
|
+
7 * 4
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
if (attrs.texIndex !== -1) {
|
|
1468
|
+
this.gl.enableVertexAttribArray(attrs.texIndex);
|
|
1469
|
+
this.gl.vertexAttribPointer(
|
|
1470
|
+
attrs.texIndex,
|
|
1471
|
+
1,
|
|
1472
|
+
this.gl.FLOAT,
|
|
1473
|
+
false,
|
|
1474
|
+
stride,
|
|
1475
|
+
11 * 4
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
if (this.texture) {
|
|
1479
|
+
this.texture.bind(0);
|
|
1480
|
+
const textureUniform = this.shader.getUniformLocation("uTexture");
|
|
1481
|
+
if (textureUniform !== null) {
|
|
1482
|
+
this.gl.uniform1i(textureUniform, 0);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const tileSizeUniform = this.shader.getUniformLocation("uTileSize");
|
|
1486
|
+
if (tileSizeUniform !== null) {
|
|
1487
|
+
this.gl.uniform2f(tileSizeUniform, this.tileSize.width, this.tileSize.height);
|
|
1488
|
+
}
|
|
1489
|
+
const cameraUniform = this.shader.getUniformLocation("uCamera");
|
|
1490
|
+
if (cameraUniform !== null) {
|
|
1491
|
+
this.gl.uniform3f(cameraUniform, camera.x, camera.y, camera.zoom);
|
|
1492
|
+
}
|
|
1493
|
+
const zScaleUniform = this.shader.getUniformLocation("uZScale");
|
|
1494
|
+
if (zScaleUniform !== null) {
|
|
1495
|
+
this.gl.uniform1f(zScaleUniform, this.zScale);
|
|
1496
|
+
}
|
|
1497
|
+
const resolutionUniform = this.shader.getUniformLocation("uResolution");
|
|
1498
|
+
if (resolutionUniform !== null) {
|
|
1499
|
+
this.gl.uniform2f(resolutionUniform, this.resolution.width, this.resolution.height);
|
|
1500
|
+
}
|
|
1501
|
+
const rotationUniform = this.shader.getUniformLocation("uRotation");
|
|
1502
|
+
if (rotationUniform !== null) {
|
|
1503
|
+
this.gl.uniform1f(rotationUniform, 0);
|
|
1504
|
+
}
|
|
1505
|
+
const quadSizeUniform = this.shader.getUniformLocation("uQuadSize");
|
|
1506
|
+
if (quadSizeUniform !== null) {
|
|
1507
|
+
this.gl.uniform2f(quadSizeUniform, 1, 1);
|
|
1508
|
+
}
|
|
1509
|
+
const vertexCount = this.quads.length * this.verticesPerQuad;
|
|
1510
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, vertexCount);
|
|
1511
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Clean up GPU resources
|
|
1516
|
+
*/
|
|
1517
|
+
dispose() {
|
|
1518
|
+
if (this.vertexBuffer) {
|
|
1519
|
+
this.gl.deleteBuffer(this.vertexBuffer);
|
|
1520
|
+
this.vertexBuffer = null;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1135
1524
|
class Matrix4 {
|
|
1136
1525
|
/**
|
|
1137
1526
|
* Create an identity matrix
|
|
@@ -1399,6 +1788,14 @@ class Camera {
|
|
|
1399
1788
|
return { x: screenX, y: screenY };
|
|
1400
1789
|
}
|
|
1401
1790
|
}
|
|
1791
|
+
class ProjectionConfig {
|
|
1792
|
+
// Scale factor for height (vertical exaggeration)
|
|
1793
|
+
constructor(tileWidth = 64, tileHeight = 32, zScale = 1) {
|
|
1794
|
+
this.tileWidth = tileWidth;
|
|
1795
|
+
this.tileHeight = tileHeight;
|
|
1796
|
+
this.zScale = zScale;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1402
1799
|
class BrowserResourceLoader {
|
|
1403
1800
|
/**
|
|
1404
1801
|
* Create a new browser resource loader
|
|
@@ -2341,14 +2738,17 @@ export {
|
|
|
2341
2738
|
BrowserResourceLoader,
|
|
2342
2739
|
Camera,
|
|
2343
2740
|
Environment,
|
|
2741
|
+
GPUBasedSpriteBatchRenderer,
|
|
2344
2742
|
GraphicsDevice,
|
|
2345
2743
|
IndexBuffer,
|
|
2346
2744
|
Matrix4,
|
|
2347
2745
|
NodeRenderingContext,
|
|
2348
2746
|
NodeResourceLoader,
|
|
2747
|
+
ProjectionConfig,
|
|
2349
2748
|
RenderingContextFactory,
|
|
2350
2749
|
ResourceLoaderFactory,
|
|
2351
2750
|
ResourcePipeline,
|
|
2751
|
+
SDLWindow,
|
|
2352
2752
|
Shader,
|
|
2353
2753
|
SpriteBatchRenderer,
|
|
2354
2754
|
Texture,
|