@gg-web-engine/core 0.0.59 → 0.0.60
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/2d/gg-2d-world.js +53 -0
- package/dist/3d/gg-3d-world.js +73 -0
- package/dist/3d/loader.js +10 -1
- package/dist/3d/models/gg-meta.d.ts +10 -0
- package/dist/3d/models/gg-meta.js +2 -1
- package/dist/base/clock/pausable-clock.d.ts +12 -0
- package/dist/base/clock/pausable-clock.js +24 -0
- package/dist/base/gg-world.js +60 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -9
- package/tsconfig.json +2 -1
- package/blender_exporter/build_blender_scene.py +0 -179
- package/blender_exporter/spherical_to_cube_texture.py +0 -116
package/dist/2d/gg-2d-world.js
CHANGED
|
@@ -37,6 +37,59 @@ export class Gg2dWorld extends GgWorld {
|
|
|
37
37
|
}
|
|
38
38
|
registerConsoleCommands(ggstatic) {
|
|
39
39
|
super.registerConsoleCommands(ggstatic);
|
|
40
|
+
ggstatic.registerConsoleCommand(this, 'set_position', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
const [name, x, y] = args;
|
|
42
|
+
if (!name) {
|
|
43
|
+
throw new Error('usage: set_position <name> <x> <y>');
|
|
44
|
+
}
|
|
45
|
+
const entity = this.getEntityByName(name);
|
|
46
|
+
if (!('position' in entity)) {
|
|
47
|
+
throw new Error(`Entity "${name}" (${entity.constructor.name}) has no position`);
|
|
48
|
+
}
|
|
49
|
+
if ([x, y].some(v => v === undefined || isNaN(+v))) {
|
|
50
|
+
throw new Error('usage: set_position <name> <x> <y>');
|
|
51
|
+
}
|
|
52
|
+
entity.position = { x: +x, y: +y };
|
|
53
|
+
return JSON.stringify(entity.position);
|
|
54
|
+
}), 'args: [ string, float, float ]; Teleport a named entity to world-space coordinates. Use ' +
|
|
55
|
+
'"entities"/"entity <name>" to find entity names and their current position');
|
|
56
|
+
ggstatic.registerConsoleCommand(this, 'set_rotation', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
const [name, angle] = args;
|
|
58
|
+
if (!name) {
|
|
59
|
+
throw new Error('usage: set_rotation <name> <angleRadians>');
|
|
60
|
+
}
|
|
61
|
+
const entity = this.getEntityByName(name);
|
|
62
|
+
if (!('rotation' in entity)) {
|
|
63
|
+
throw new Error(`Entity "${name}" (${entity.constructor.name}) has no rotation`);
|
|
64
|
+
}
|
|
65
|
+
if (angle === undefined || isNaN(+angle)) {
|
|
66
|
+
throw new Error('usage: set_rotation <name> <angleRadians>');
|
|
67
|
+
}
|
|
68
|
+
entity.rotation = +angle;
|
|
69
|
+
return JSON.stringify(entity.rotation);
|
|
70
|
+
}), 'args: [ string, float ]; Rotate a named entity to the given angle, in radians');
|
|
71
|
+
ggstatic.registerConsoleCommand(this, 'spawn', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
const [shapeArg, x, y, dynamicArg] = args;
|
|
73
|
+
if ([x, y].some(v => v === undefined || isNaN(+v))) {
|
|
74
|
+
throw new Error('usage: spawn <SQUARE|CIRCLE> <x> <y> [dynamic=0|1]');
|
|
75
|
+
}
|
|
76
|
+
const dynamic = dynamicArg === undefined ? true : dynamicArg === '1';
|
|
77
|
+
let shape;
|
|
78
|
+
switch ((shapeArg || '').toUpperCase()) {
|
|
79
|
+
case 'SQUARE':
|
|
80
|
+
shape = { shape: 'SQUARE', dimensions: { x: 1, y: 1 } };
|
|
81
|
+
break;
|
|
82
|
+
case 'CIRCLE':
|
|
83
|
+
shape = { shape: 'CIRCLE', radius: 0.5 };
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Unknown shape "${shapeArg}". Use SQUARE|CIRCLE`);
|
|
87
|
+
}
|
|
88
|
+
const entity = this.addPrimitiveRigidBody({ shape, body: { dynamic } }, { x: +x, y: +y });
|
|
89
|
+
return `spawned "${entity.name}" (${shape.shape}) at ${JSON.stringify(entity.position)}`;
|
|
90
|
+
}), 'args: [ SQUARE|CIRCLE, float, float, 0|1? ]; Spawn a default-sized primitive rigid body at ' +
|
|
91
|
+
'world-space coordinates, for probing physics. dynamic (last arg) defaults to 1 (falls ' +
|
|
92
|
+
'under gravity); pass 0 for a static prop');
|
|
40
93
|
if (this.physicsWorld) {
|
|
41
94
|
ggstatic.registerConsoleCommand(this, 'gravity', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
42
95
|
if (args.length == 1) {
|
package/dist/3d/gg-3d-world.js
CHANGED
|
@@ -37,6 +37,79 @@ export class Gg3dWorld extends GgWorld {
|
|
|
37
37
|
}
|
|
38
38
|
registerConsoleCommands(ggstatic) {
|
|
39
39
|
super.registerConsoleCommands(ggstatic);
|
|
40
|
+
ggstatic.registerConsoleCommand(this, 'set_position', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
const [name, x, y, z] = args;
|
|
42
|
+
if (!name) {
|
|
43
|
+
throw new Error('usage: set_position <name> <x> <y> <z>');
|
|
44
|
+
}
|
|
45
|
+
const entity = this.getEntityByName(name);
|
|
46
|
+
if (!('position' in entity)) {
|
|
47
|
+
throw new Error(`Entity "${name}" (${entity.constructor.name}) has no position`);
|
|
48
|
+
}
|
|
49
|
+
if ([x, y, z].some(v => v === undefined || isNaN(+v))) {
|
|
50
|
+
throw new Error('usage: set_position <name> <x> <y> <z>');
|
|
51
|
+
}
|
|
52
|
+
entity.position = { x: +x, y: +y, z: +z };
|
|
53
|
+
return JSON.stringify(entity.position);
|
|
54
|
+
}), 'args: [ string, float, float, float ]; Teleport a named entity to world-space coordinates. ' +
|
|
55
|
+
'Use "entities"/"entity <name>" to find entity names and their current position');
|
|
56
|
+
ggstatic.registerConsoleCommand(this, 'set_rotation', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
const [name, ...rest] = args;
|
|
58
|
+
if (!name) {
|
|
59
|
+
throw new Error('usage: set_rotation <name> <x> <y> <z> [w]');
|
|
60
|
+
}
|
|
61
|
+
const entity = this.getEntityByName(name);
|
|
62
|
+
if (!('rotation' in entity)) {
|
|
63
|
+
throw new Error(`Entity "${name}" (${entity.constructor.name}) has no rotation`);
|
|
64
|
+
}
|
|
65
|
+
const nums = rest.map(Number);
|
|
66
|
+
if (nums.length !== 3 && nums.length !== 4) {
|
|
67
|
+
throw new Error('usage: set_rotation <name> <x> <y> <z> (euler, radians) OR set_rotation <name> <x> <y> <z> <w> (quaternion)');
|
|
68
|
+
}
|
|
69
|
+
if (nums.some(Number.isNaN)) {
|
|
70
|
+
throw new Error('Wrong arguments');
|
|
71
|
+
}
|
|
72
|
+
const rotation = nums.length === 4
|
|
73
|
+
? { x: nums[0], y: nums[1], z: nums[2], w: nums[3] }
|
|
74
|
+
: Qtrn.fromEuler({ x: nums[0], y: nums[1], z: nums[2] });
|
|
75
|
+
entity.rotation = rotation;
|
|
76
|
+
return JSON.stringify(entity.rotation);
|
|
77
|
+
}), 'args: [ string, float, float, float, float? ]; Rotate a named entity. 3 numbers are euler ' +
|
|
78
|
+
'angles in radians, 4 numbers are a raw quaternion (x y z w)');
|
|
79
|
+
ggstatic.registerConsoleCommand(this, 'spawn', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
const [shapeArg, x, y, z, dynamicArg] = args;
|
|
81
|
+
if ([x, y, z].some(v => v === undefined || isNaN(+v))) {
|
|
82
|
+
throw new Error('usage: spawn <BOX|SPHERE|CYLINDER|CONE|CAPSULE|PLANE> <x> <y> <z> [dynamic=0|1]');
|
|
83
|
+
}
|
|
84
|
+
const dynamic = dynamicArg === undefined ? true : dynamicArg === '1';
|
|
85
|
+
let shape;
|
|
86
|
+
switch ((shapeArg || '').toUpperCase()) {
|
|
87
|
+
case 'BOX':
|
|
88
|
+
shape = { shape: 'BOX', dimensions: { x: 1, y: 1, z: 1 } };
|
|
89
|
+
break;
|
|
90
|
+
case 'SPHERE':
|
|
91
|
+
shape = { shape: 'SPHERE', radius: 0.5 };
|
|
92
|
+
break;
|
|
93
|
+
case 'CYLINDER':
|
|
94
|
+
shape = { shape: 'CYLINDER', radius: 0.5, height: 1 };
|
|
95
|
+
break;
|
|
96
|
+
case 'CONE':
|
|
97
|
+
shape = { shape: 'CONE', radius: 0.5, height: 1 };
|
|
98
|
+
break;
|
|
99
|
+
case 'CAPSULE':
|
|
100
|
+
shape = { shape: 'CAPSULE', radius: 0.5, centersDistance: 1 };
|
|
101
|
+
break;
|
|
102
|
+
case 'PLANE':
|
|
103
|
+
shape = { shape: 'PLANE' };
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`Unknown shape "${shapeArg}". Use BOX|SPHERE|CYLINDER|CONE|CAPSULE|PLANE`);
|
|
107
|
+
}
|
|
108
|
+
const entity = this.addPrimitiveRigidBody({ shape, body: { dynamic } }, { x: +x, y: +y, z: +z });
|
|
109
|
+
return `spawned "${entity.name}" (${shape.shape}) at ${JSON.stringify(entity.position)}`;
|
|
110
|
+
}), 'args: [ BOX|SPHERE|CYLINDER|CONE|CAPSULE|PLANE, float, float, float, 0|1? ]; Spawn a ' +
|
|
111
|
+
'default-sized primitive rigid body at world-space coordinates, for probing physics. ' +
|
|
112
|
+
'dynamic (last arg) defaults to 1 (falls under gravity); pass 0 for a static prop');
|
|
40
113
|
if (this.physicsWorld) {
|
|
41
114
|
ggstatic.registerConsoleCommand(this, 'gravity', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
42
115
|
if (args.length == 1) {
|
package/dist/3d/loader.js
CHANGED
|
@@ -7,6 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
import { GG_META_SUPPORTED_FORMAT_VERSION } from './models/gg-meta';
|
|
10
11
|
import { Entity3d } from './entities/entity-3d';
|
|
11
12
|
import { GroupEntity, Pnt3, Qtrn } from '../base';
|
|
12
13
|
import { Gg3dLevelLoader } from './level-loader';
|
|
@@ -71,7 +72,15 @@ export class Gg3dLoader extends Gg3dLevelLoader {
|
|
|
71
72
|
fetch(`${path}.glb`).then(r => r.arrayBuffer()),
|
|
72
73
|
fetch(`${path}.meta`)
|
|
73
74
|
.then(r => r.text())
|
|
74
|
-
.then(r => JSON.parse(r))
|
|
75
|
+
.then(r => JSON.parse(r))
|
|
76
|
+
.then((meta) => {
|
|
77
|
+
if (meta.formatVersion !== undefined && meta.formatVersion > GG_META_SUPPORTED_FORMAT_VERSION) {
|
|
78
|
+
console.warn(`${path}.meta declares formatVersion ${meta.formatVersion}, but this build of ` +
|
|
79
|
+
`@gg-web-engine/core only understands up to ${GG_META_SUPPORTED_FORMAT_VERSION}. ` +
|
|
80
|
+
`Update @gg-web-engine/core, or re-export with an older version of the GG Web Engine Exporter add-on.`);
|
|
81
|
+
}
|
|
82
|
+
return meta;
|
|
83
|
+
}),
|
|
75
84
|
]);
|
|
76
85
|
if (useCache) {
|
|
77
86
|
this.filesCache.set(path, loadPromise);
|
|
@@ -16,7 +16,17 @@ export type GgRigidBody = {
|
|
|
16
16
|
rotation: Point4;
|
|
17
17
|
} & BodyShape3DDescriptor;
|
|
18
18
|
export type GgMeta = {
|
|
19
|
+
/**
|
|
20
|
+
* Written by the Blender exporter (`GG_META_FORMAT_VERSION` in
|
|
21
|
+
* `blender-addon/gg_web_engine_exporter/exporter.py`) since it started declaring one. Absent on
|
|
22
|
+
* `.meta` files exported before that, which is fine - the shape hasn't actually changed yet, so
|
|
23
|
+
* there is nothing to migrate; this only matters once a future export starts writing a `.meta`
|
|
24
|
+
* this loader's current version doesn't understand.
|
|
25
|
+
*/
|
|
26
|
+
formatVersion?: number;
|
|
19
27
|
dummies: GgDummy[];
|
|
20
28
|
curves: GgCurve[];
|
|
21
29
|
rigidBodies: GgRigidBody[];
|
|
22
30
|
};
|
|
31
|
+
/** Highest `.meta` `formatVersion` this loader understands - see `GgMeta.formatVersion`. */
|
|
32
|
+
export declare const GG_META_SUPPORTED_FORMAT_VERSION = 1;
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** Highest `.meta` `formatVersion` this loader understands - see `GgMeta.formatVersion`. */
|
|
2
|
+
export const GG_META_SUPPORTED_FORMAT_VERSION = 1;
|
|
@@ -61,6 +61,18 @@ export declare class PausableClock extends IClock {
|
|
|
61
61
|
* Pauses the clock.
|
|
62
62
|
*/
|
|
63
63
|
pause(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Fires exactly one tick with the given delta while the clock is paused, without resuming it -
|
|
66
|
+
* useful for frame-by-frame debugging. `elapsedTime` (and everything derived from it - child
|
|
67
|
+
* clocks, animations, anything reading `this.elapsedTime`) advances by exactly `delta`, same as
|
|
68
|
+
* it would over `delta` worth of normal ticking, and stays at that new instant once `step`
|
|
69
|
+
* returns (the clock is still paused, it just moved its frozen instant forward). A manual step
|
|
70
|
+
* is never throttled by `tickRateLimit`, and resuming afterwards continues seamlessly from the
|
|
71
|
+
* stepped-to instant rather than losing or double-counting the stepped time.
|
|
72
|
+
* @param delta - tick delta to report to subscribers, in milliseconds
|
|
73
|
+
* @throws if the clock isn't currently paused
|
|
74
|
+
*/
|
|
75
|
+
step(delta: number): void;
|
|
64
76
|
/**
|
|
65
77
|
* Resumes the clock.
|
|
66
78
|
*/
|
|
@@ -126,6 +126,30 @@ export class PausableClock extends IClock {
|
|
|
126
126
|
this.pausedByTimescale = false;
|
|
127
127
|
this.paused$.next(true);
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Fires exactly one tick with the given delta while the clock is paused, without resuming it -
|
|
131
|
+
* useful for frame-by-frame debugging. `elapsedTime` (and everything derived from it - child
|
|
132
|
+
* clocks, animations, anything reading `this.elapsedTime`) advances by exactly `delta`, same as
|
|
133
|
+
* it would over `delta` worth of normal ticking, and stays at that new instant once `step`
|
|
134
|
+
* returns (the clock is still paused, it just moved its frozen instant forward). A manual step
|
|
135
|
+
* is never throttled by `tickRateLimit`, and resuming afterwards continues seamlessly from the
|
|
136
|
+
* stepped-to instant rather than losing or double-counting the stepped time.
|
|
137
|
+
* @param delta - tick delta to report to subscribers, in milliseconds
|
|
138
|
+
* @throws if the clock isn't currently paused
|
|
139
|
+
*/
|
|
140
|
+
step(delta) {
|
|
141
|
+
if (!this.isPaused) {
|
|
142
|
+
throw new Error('Clock must be paused to step it manually');
|
|
143
|
+
}
|
|
144
|
+
// advance the frozen instant so `elapsedTime` (= timeScale * (pausedAt - startedAt)) grows by
|
|
145
|
+
// exactly `delta`, matching what `delta` means for every other tick consumer
|
|
146
|
+
this.pausedAt += this._timeScale !== 0 ? delta / this._timeScale : delta;
|
|
147
|
+
// keep the internal relative-time bookkeeping in step, so the first real tick after resume
|
|
148
|
+
// reports a normal-sized delta instead of one that swallows the whole stepped duration
|
|
149
|
+
this.oldRelativeTime += delta;
|
|
150
|
+
this.lastFiredTickElapsed = this.oldRelativeTime;
|
|
151
|
+
this._tick$.next([this.elapsedTime, delta]);
|
|
152
|
+
}
|
|
129
153
|
/**
|
|
130
154
|
* Resumes the clock.
|
|
131
155
|
*/
|
package/dist/base/gg-world.js
CHANGED
|
@@ -196,6 +196,19 @@ export class GgWorld {
|
|
|
196
196
|
}
|
|
197
197
|
return this.worldClock.tickRateLimit.toString();
|
|
198
198
|
}), 'args: [ int? ]; Get current tick rate limit of selected world clock or set it. 0 means no limit applied');
|
|
199
|
+
ggstatic.registerConsoleCommand(this, 'step', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
200
|
+
if (!this.worldClock.isPaused) {
|
|
201
|
+
throw new Error('World must be paused first (run "timescale 0") before it can be stepped');
|
|
202
|
+
}
|
|
203
|
+
const ms = args[0] === undefined ? 1000 / 120 : +args[0];
|
|
204
|
+
if (isNaN(ms) || ms <= 0) {
|
|
205
|
+
throw new Error('usage: step [ms]; ms must be a positive number');
|
|
206
|
+
}
|
|
207
|
+
this.worldClock.step(ms);
|
|
208
|
+
return `stepped ${ms} ms`;
|
|
209
|
+
}), 'args: [ float? ]; Advance a paused world clock by exactly one tick of the given duration ' +
|
|
210
|
+
'in milliseconds (default 8, i.e. 1000/120). Only works while the world is paused via ' +
|
|
211
|
+
'"timescale 0"; rejects otherwise');
|
|
199
212
|
ggstatic.registerConsoleCommand(this, 'renderers', () => __awaiter(this, void 0, void 0, function* () {
|
|
200
213
|
return this.renderers.map(r => r.name).join('\n');
|
|
201
214
|
}), 'no args; Print all renderers in selected world');
|
|
@@ -258,6 +271,53 @@ export class GgWorld {
|
|
|
258
271
|
}), 'args: [ int?, avg|peak? ]; Measure how much time was spent per ' +
|
|
259
272
|
'entity in world. Arguments are samples amount (20 by default) and "peak" or "avg" choice, both arguments are ' +
|
|
260
273
|
'optional. "avg" report sorts entities by average time consumed, "peak" records highest value for each entity');
|
|
274
|
+
ggstatic.registerConsoleCommand(this, 'entities', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
275
|
+
var _a;
|
|
276
|
+
const filter = (_a = args[0]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
277
|
+
const list = this.children.filter(e => !filter || e.name.toLowerCase().includes(filter));
|
|
278
|
+
if (list.length === 0) {
|
|
279
|
+
return '<span style="color:#aaa">(no entities)</span>';
|
|
280
|
+
}
|
|
281
|
+
return list
|
|
282
|
+
.map(e => `<span style='color:yellow'>${e.name}</span>\t<span style='color:#aaa'>${e.constructor.name}</span>`)
|
|
283
|
+
.join('\n');
|
|
284
|
+
}), 'args: [ string? ]; List all entities in this world (name and class), optionally filtered by ' +
|
|
285
|
+
'a case-insensitive substring of the name. Use "entity <name>" to inspect one of them');
|
|
286
|
+
ggstatic.registerConsoleCommand(this, 'entity', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
287
|
+
const name = args[0];
|
|
288
|
+
if (!name) {
|
|
289
|
+
throw new Error('usage: entity <name>; use "entities" to list available names');
|
|
290
|
+
}
|
|
291
|
+
const entity = this.getEntityByName(name);
|
|
292
|
+
const lines = [
|
|
293
|
+
`class: ${entity.constructor.name}`,
|
|
294
|
+
`active: ${entity.active}`,
|
|
295
|
+
`parent: ${entity.parent ? entity.parent.name : '(none)'}`,
|
|
296
|
+
];
|
|
297
|
+
if ('visible' in entity) {
|
|
298
|
+
lines.push(`visible: ${entity.visible}`);
|
|
299
|
+
}
|
|
300
|
+
if ('position' in entity) {
|
|
301
|
+
lines.push(`position: ${JSON.stringify(entity.position)}`);
|
|
302
|
+
}
|
|
303
|
+
if ('rotation' in entity) {
|
|
304
|
+
lines.push(`rotation: ${JSON.stringify(entity.rotation)}`);
|
|
305
|
+
}
|
|
306
|
+
lines.push(`children: ${entity.children.length === 0 ? '(none)' : entity.children.map(c => c.name).join(', ')}`);
|
|
307
|
+
return lines.join('\n');
|
|
308
|
+
}), 'args: [ string ]; Print class, position/rotation (if any) and children of one entity. Use ' +
|
|
309
|
+
'"entities" to list available names, "set_position"/"set_rotation" to move it');
|
|
310
|
+
ggstatic.registerConsoleCommand(this, 'remove', (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
311
|
+
const name = args[0];
|
|
312
|
+
if (!name) {
|
|
313
|
+
throw new Error('usage: remove <name> [dispose=0|1]');
|
|
314
|
+
}
|
|
315
|
+
const entity = this.getEntityByName(name);
|
|
316
|
+
const dispose = args[1] === undefined ? true : args[1] === '1';
|
|
317
|
+
this.removeEntity(entity, dispose);
|
|
318
|
+
return `removed "${name}"`;
|
|
319
|
+
}), 'args: [ string, 0|1? ]; Remove the named entity from this world, disposing it by default. ' +
|
|
320
|
+
'Pass 0 as second arg to detach without disposing (e.g. before re-adding it elsewhere)');
|
|
261
321
|
}
|
|
262
322
|
}
|
|
263
323
|
GgWorld.default_name_counter = 0;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.0.
|
|
1
|
+
export declare const VERSION = "0.0.60";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.0.
|
|
1
|
+
export const VERSION = '0.0.60';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gg-web-engine/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.60",
|
|
4
4
|
"description": "An attempt to create open source game engine for browser",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -28,17 +28,18 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://github.com/AndyGura/gg-web-engine#readme",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"rxjs": "7.8.
|
|
31
|
+
"rxjs": "7.8.2",
|
|
32
32
|
"stats.js": "0.17.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@types/jest": "^
|
|
36
|
-
"@types/
|
|
37
|
-
"
|
|
38
|
-
"jest
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
35
|
+
"@types/jest": "^30.0.0",
|
|
36
|
+
"@types/node": "^26.4.0",
|
|
37
|
+
"@types/stats.js": "^0.17.4",
|
|
38
|
+
"jest": "^30.5.0",
|
|
39
|
+
"jest-environment-jsdom": "^30.5.0",
|
|
40
|
+
"prettier": "^3.9.6",
|
|
41
|
+
"ts-jest": "^29.4.12",
|
|
42
|
+
"typescript": "~6.0.3"
|
|
42
43
|
},
|
|
43
44
|
"jest": {
|
|
44
45
|
"moduleFileExtensions": [
|
package/tsconfig.json
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
"baseUrl": "./src/",
|
|
5
5
|
"outDir": "./dist/",
|
|
6
6
|
"rootDir": "./src/",
|
|
7
|
-
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
|
7
|
+
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
|
|
8
|
+
"types": ["jest", "node"] /* TS6 no longer auto-includes every @types/* package found by walking up typeRoots when "types" is unset (TS5 did) — declare what test code under test/ actually needs (jest globals, Node's `global`) explicitly rather than relying on the old implicit-hoisting behavior. */
|
|
8
9
|
},
|
|
9
10
|
"include": ["*.ts", "**/*.ts"],
|
|
10
11
|
"exclude": ["**/*.spec.ts", "node_modules", "dist/**/*", "test/**/*"]
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import tempfile
|
|
4
|
-
|
|
5
|
-
from string import Template
|
|
6
|
-
|
|
7
|
-
__blender_script_template = Template("""
|
|
8
|
-
import bpy
|
|
9
|
-
import json
|
|
10
|
-
import os
|
|
11
|
-
from rna_prop_ui import rna_idprop_value_to_python
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def parse_curve_obj(object):
|
|
15
|
-
curve = bpy.data.curves[object.data.name]
|
|
16
|
-
spline = curve.splines[0]
|
|
17
|
-
is_cyclic = spline.use_cyclic_u
|
|
18
|
-
points = list(map(lambda p: {
|
|
19
|
-
"x": p.co.x + object.location.x,
|
|
20
|
-
"y": p.co.y + object.location.y,
|
|
21
|
-
"z": p.co.z + object.location.z
|
|
22
|
-
}, spline.points))
|
|
23
|
-
return {
|
|
24
|
-
"name": object.name,
|
|
25
|
-
"cyclic": is_cyclic,
|
|
26
|
-
"points": points,
|
|
27
|
-
**{x: rna_idprop_value_to_python(object[x]) for x in object.keys() if x != '_RNA_UI'}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
def parse_dummy_obj(object):
|
|
31
|
-
object.rotation_mode = 'QUATERNION'
|
|
32
|
-
return {
|
|
33
|
-
"name": object.name,
|
|
34
|
-
"position": {
|
|
35
|
-
"x": object.location.x,
|
|
36
|
-
"y": object.location.y,
|
|
37
|
-
"z": object.location.z,
|
|
38
|
-
},
|
|
39
|
-
"rotation": {
|
|
40
|
-
"x": object.rotation_quaternion.x,
|
|
41
|
-
"y": object.rotation_quaternion.y,
|
|
42
|
-
"z": object.rotation_quaternion.z,
|
|
43
|
-
"w": object.rotation_quaternion.w,
|
|
44
|
-
},
|
|
45
|
-
**{x: object[x] for x in object.keys() if x != '_RNA_UI'}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
def get_rigid_body_description(obj, export_body_parameters=True):
|
|
49
|
-
body = obj.rigid_body
|
|
50
|
-
obj.rotation_mode = 'QUATERNION'
|
|
51
|
-
meta = {
|
|
52
|
-
"name": obj.name,
|
|
53
|
-
"position": {
|
|
54
|
-
'x': obj.location.x,
|
|
55
|
-
'y': obj.location.y,
|
|
56
|
-
'z': obj.location.z,
|
|
57
|
-
},
|
|
58
|
-
# FIXME relative rotation
|
|
59
|
-
"rotation": { 'x': obj.rotation_quaternion.x, 'y': obj.rotation_quaternion.y, 'z': obj.rotation_quaternion.z, 'w': obj.rotation_quaternion.w },
|
|
60
|
-
"shape": {
|
|
61
|
-
"shape": body.collision_shape,
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
if (export_body_parameters):
|
|
65
|
-
meta['body'] = {
|
|
66
|
-
"dynamic": body.type == "ACTIVE",
|
|
67
|
-
"mass": body.mass,
|
|
68
|
-
"restitution": body.restitution,
|
|
69
|
-
"friction": body.friction,
|
|
70
|
-
}
|
|
71
|
-
parent = obj.parent
|
|
72
|
-
while parent:
|
|
73
|
-
meta['position']['x'] -= parent.location.x
|
|
74
|
-
meta['position']['y'] -= parent.location.y
|
|
75
|
-
meta['position']['z'] -= parent.location.z
|
|
76
|
-
parent = parent.parent
|
|
77
|
-
meta['position']['x'] = round(meta['position']['x'], 6)
|
|
78
|
-
meta['position']['y'] = round(meta['position']['y'], 6)
|
|
79
|
-
meta['position']['z'] = round(meta['position']['z'], 6)
|
|
80
|
-
if meta['shape']['shape'] == 'SPHERE':
|
|
81
|
-
meta['shape']['radius'] = max(obj.dimensions.x, obj.dimensions.y, obj.dimensions.z) / 2
|
|
82
|
-
elif meta['shape']['shape'] == 'BOX':
|
|
83
|
-
meta['shape']['dimensions'] = { 'x': obj.dimensions.x, 'y': obj.dimensions.y, 'z': obj.dimensions.z }
|
|
84
|
-
elif meta['shape']['shape'] in ['CONE', 'CYLINDER']:
|
|
85
|
-
meta['shape']['radius'] = max(obj.dimensions.x, obj.dimensions.y) / 2
|
|
86
|
-
meta['shape']['height'] = obj.dimensions.z
|
|
87
|
-
elif meta['shape']['shape'] == 'CAPSULE':
|
|
88
|
-
meta['shape']['radius'] = max(obj.dimensions.x, obj.dimensions.y) / 2
|
|
89
|
-
meta['shape']['centersDistance'] = obj.dimensions.z - max(obj.dimensions.x, obj.dimensions.y)
|
|
90
|
-
elif meta['shape']['shape'] == 'CONVEX_HULL':
|
|
91
|
-
meta['shape']['vertices'] = [{ 'x': v.co.x, 'y': v.co.y, 'z': v.co.z } for v in obj.data.vertices]
|
|
92
|
-
elif meta['shape']['shape'] == 'MESH':
|
|
93
|
-
meta['shape']['vertices'] = [{ 'x': v.co.x, 'y': v.co.y, 'z': v.co.z } for v in obj.data.vertices]
|
|
94
|
-
import bmesh
|
|
95
|
-
bm = bmesh.new()
|
|
96
|
-
bm.from_mesh(obj.data)
|
|
97
|
-
bmesh.ops.triangulate(bm, faces=bm.faces[:])
|
|
98
|
-
meta['shape']['faces'] = [[v.index for v in f.verts] for f in bm.faces]
|
|
99
|
-
bm.free()
|
|
100
|
-
elif meta['shape']['shape'] == 'COMPOUND':
|
|
101
|
-
meta['shape']['children'] = [get_rigid_body_description(sub_obj, export_body_parameters=False)
|
|
102
|
-
for sub_obj in bpy.context.scene.objects
|
|
103
|
-
if sub_obj.rigid_body is not None and sub_obj.parent == obj]
|
|
104
|
-
else:
|
|
105
|
-
raise NotImplementedError(f'GG does not support exporting rigid body {meta["shape"]["shape"]} shape')
|
|
106
|
-
return meta
|
|
107
|
-
|
|
108
|
-
if "$src_file":
|
|
109
|
-
bpy.ops.wm.open_mainfile(filepath="$src_file")
|
|
110
|
-
|
|
111
|
-
# compound metadata
|
|
112
|
-
metadata = {"curves": [], "dummies": [], "rigidBodies": []}
|
|
113
|
-
for obj in filter(lambda x: x.type == "CURVE", bpy.data.objects):
|
|
114
|
-
metadata["curves"].append(parse_curve_obj(obj))
|
|
115
|
-
for obj in filter(lambda x: x.type == "EMPTY", bpy.data.objects):
|
|
116
|
-
metadata["dummies"].append(parse_dummy_obj(obj))
|
|
117
|
-
|
|
118
|
-
# saving scene
|
|
119
|
-
for obj in bpy.context.scene.objects:
|
|
120
|
-
include_in_export = obj.type in ["MESH", "LIGHT", "CURVE"]
|
|
121
|
-
include_in_glb = not obj.hide_render
|
|
122
|
-
obj.select_set(state=include_in_export and include_in_glb)
|
|
123
|
-
if not include_in_export:
|
|
124
|
-
continue
|
|
125
|
-
for modifier in obj.modifiers:
|
|
126
|
-
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
|
127
|
-
if obj.rigid_body is not None and (obj.parent is None or obj.parent.rigid_body is None or obj.parent.rigid_body.collision_shape != 'COMPOUND'):
|
|
128
|
-
metadata["rigidBodies"].append(get_rigid_body_description(obj))
|
|
129
|
-
|
|
130
|
-
bpy.ops.export_scene.gltf(export_format="GLB",
|
|
131
|
-
export_copyright="Gurakl Games",
|
|
132
|
-
export_texcoords=True,
|
|
133
|
-
export_normals=True,
|
|
134
|
-
export_tangents=True,
|
|
135
|
-
export_materials='$export_materials',
|
|
136
|
-
export_cameras=False,
|
|
137
|
-
export_lights=True,
|
|
138
|
-
export_extras=True,
|
|
139
|
-
export_yup=False,
|
|
140
|
-
export_apply=False,
|
|
141
|
-
export_animations=False,
|
|
142
|
-
use_selection=True,
|
|
143
|
-
export_skins=False,
|
|
144
|
-
export_morph=False,
|
|
145
|
-
filepath="$file_name.glb")
|
|
146
|
-
# saving metadata
|
|
147
|
-
with open("$file_name.meta", 'w') as outfile:
|
|
148
|
-
json.dump(metadata, outfile)""")
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def construct_blender_export_script(file_name, src_file="", export_materials="EXPORT") -> str:
|
|
152
|
-
return __blender_script_template.substitute({
|
|
153
|
-
'src_file': src_file,
|
|
154
|
-
'file_name': file_name,
|
|
155
|
-
'export_materials': export_materials
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
if __name__ == "__main__":
|
|
159
|
-
# will be invoked if this module is being run directly, but not via import
|
|
160
|
-
skip_textures = sys.argv[1] == '--skip-textures'
|
|
161
|
-
if skip_textures:
|
|
162
|
-
files = sys.argv[2:]
|
|
163
|
-
else:
|
|
164
|
-
files = sys.argv[1:]
|
|
165
|
-
|
|
166
|
-
script = ""
|
|
167
|
-
for file in files:
|
|
168
|
-
if file.startswith('./'):
|
|
169
|
-
file = file[2:]
|
|
170
|
-
full_path = os.path.join(os.getcwd(), file)
|
|
171
|
-
script += '\n\n\n' + construct_blender_export_script(src_file=full_path,
|
|
172
|
-
file_name=full_path[:full_path.rindex('.')],
|
|
173
|
-
export_materials="NONE" if skip_textures else "EXPORT")
|
|
174
|
-
|
|
175
|
-
script_file = tempfile.NamedTemporaryFile(delete=False, mode='w')
|
|
176
|
-
script_file.write(script)
|
|
177
|
-
script_file.flush()
|
|
178
|
-
os.system(f"blender --python {script_file.name} --background")
|
|
179
|
-
os.unlink(script_file.name)
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from math import pi, atan2, hypot, floor, ceil
|
|
3
|
-
|
|
4
|
-
from PIL import Image
|
|
5
|
-
from numpy import clip, average
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# get x,y,z coords from out image pixels coords
|
|
9
|
-
# i,j are pixel coords
|
|
10
|
-
# face is face number
|
|
11
|
-
# edge is edge length
|
|
12
|
-
def outImgToXYZ(i, j, face, edge):
|
|
13
|
-
a = 2.0 * float(i) / edge
|
|
14
|
-
b = 2.0 * float(j) / edge
|
|
15
|
-
if face == 0: # back
|
|
16
|
-
(x, y, z) = (-1.0, 1.0 - a, 3.0 - b)
|
|
17
|
-
elif face == 1: # left
|
|
18
|
-
(x, y, z) = (a - 3.0, -1.0, 3.0 - b)
|
|
19
|
-
elif face == 2: # front
|
|
20
|
-
(x, y, z) = (1.0, a - 5.0, 3.0 - b)
|
|
21
|
-
elif face == 3: # right
|
|
22
|
-
(x, y, z) = (7.0 - a, 1.0, 3.0 - b)
|
|
23
|
-
elif face == 4: # top
|
|
24
|
-
(x, y, z) = (b - 1.0, a - 5.0, 1.0)
|
|
25
|
-
elif face == 5: # bottom
|
|
26
|
-
(x, y, z) = (5.0 - b, a - 5.0, -1.0)
|
|
27
|
-
return (x, y, z)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# convert using an inverse transformation
|
|
31
|
-
def convertBack(imgIn, imgOut):
|
|
32
|
-
inSize = imgIn.size
|
|
33
|
-
outSize = imgOut.size
|
|
34
|
-
inPix = imgIn.load()
|
|
35
|
-
outPix = imgOut.load()
|
|
36
|
-
edge = int(inSize[0] / 4) # the length of each edge in pixels
|
|
37
|
-
for i in range(outSize[0]):
|
|
38
|
-
face = int(i / edge) # 0 - back, 1 - left 2 - front, 3 - right
|
|
39
|
-
if face == 2:
|
|
40
|
-
rng = range(0, edge * 3)
|
|
41
|
-
else:
|
|
42
|
-
rng = range(edge, edge * 2)
|
|
43
|
-
|
|
44
|
-
for j in rng:
|
|
45
|
-
if j < edge:
|
|
46
|
-
face2 = 4 # top
|
|
47
|
-
elif j >= 2 * edge:
|
|
48
|
-
face2 = 5 # bottom
|
|
49
|
-
else:
|
|
50
|
-
face2 = face
|
|
51
|
-
|
|
52
|
-
(x, y, z) = outImgToXYZ(i, j, face2, edge)
|
|
53
|
-
theta = atan2(y, x) # range -pi to pi
|
|
54
|
-
r = hypot(x, y)
|
|
55
|
-
phi = atan2(z, r) # range -pi/2 to pi/2
|
|
56
|
-
# source img coords
|
|
57
|
-
uf = (2.0 * edge * (theta + pi) / pi)
|
|
58
|
-
vf = (2.0 * edge * (pi / 2 - phi) / pi)
|
|
59
|
-
# Use bilinear interpolation between the four surrounding pixels
|
|
60
|
-
ui = floor(uf) # coord of pixel to bottom left
|
|
61
|
-
vi = floor(vf)
|
|
62
|
-
u2 = ui + 1 # coords of pixel to top right
|
|
63
|
-
v2 = vi + 1
|
|
64
|
-
mu = uf - ui # fraction of way across pixel
|
|
65
|
-
nu = vf - vi
|
|
66
|
-
# Pixel values of four corners
|
|
67
|
-
A = inPix[ui % inSize[0], clip(vi, 0, inSize[1] - 1)]
|
|
68
|
-
B = inPix[u2 % inSize[0], clip(vi, 0, inSize[1] - 1)]
|
|
69
|
-
C = inPix[ui % inSize[0], clip(v2, 0, inSize[1] - 1)]
|
|
70
|
-
D = inPix[u2 % inSize[0], clip(v2, 0, inSize[1] - 1)]
|
|
71
|
-
# interpolate
|
|
72
|
-
(r, g, b) = (
|
|
73
|
-
A[0] * (1 - mu) * (1 - nu) + B[0] * (mu) * (1 - nu) + C[0] * (1 - mu) * nu + D[0] * mu * nu,
|
|
74
|
-
A[1] * (1 - mu) * (1 - nu) + B[1] * (mu) * (1 - nu) + C[1] * (1 - mu) * nu + D[1] * mu * nu,
|
|
75
|
-
A[2] * (1 - mu) * (1 - nu) + B[2] * (mu) * (1 - nu) + C[2] * (1 - mu) * nu + D[2] * mu * nu)
|
|
76
|
-
|
|
77
|
-
outPix[i, j] = (int(round(r)), int(round(g)), int(round(b)))
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
files = sys.argv[1:]
|
|
81
|
-
for file_name in files:
|
|
82
|
-
imgIn = Image.open(file_name)
|
|
83
|
-
width, height = imgIn.size
|
|
84
|
-
if width < height * 2:
|
|
85
|
-
imgIn = imgIn.resize((height * 2, height), Image.ANTIALIAS)
|
|
86
|
-
elif width > height * 2:
|
|
87
|
-
imgIn = imgIn.resize((width, width / 2), Image.ANTIALIAS)
|
|
88
|
-
imgOut = Image.new("RGB", (imgIn.size[0], int(imgIn.size[0] * 3 / 4)), "black")
|
|
89
|
-
convertBack(imgIn, imgOut)
|
|
90
|
-
# imgOut.save('cubemap.png')
|
|
91
|
-
# split squares
|
|
92
|
-
file_extension = ".png"
|
|
93
|
-
|
|
94
|
-
name_map = [["", "", "pz", ""],
|
|
95
|
-
["ny", "nx", "py", "px"],
|
|
96
|
-
["", "", "nz", ""]]
|
|
97
|
-
rotation = [[0, 0, 180, 0],
|
|
98
|
-
[0, 90, 180, -90],
|
|
99
|
-
[0, 0, 0, 0]]
|
|
100
|
-
width, height = imgOut.size
|
|
101
|
-
|
|
102
|
-
cube_size = width / 4
|
|
103
|
-
|
|
104
|
-
filelist = []
|
|
105
|
-
for row in range(3):
|
|
106
|
-
for col in range(4):
|
|
107
|
-
if name_map[row][col] != "":
|
|
108
|
-
sx = cube_size * col
|
|
109
|
-
sy = cube_size * row
|
|
110
|
-
fn = file_name[:-4] + '_' + name_map[row][col] + file_extension
|
|
111
|
-
filelist.append(fn)
|
|
112
|
-
print("%s --> %s" % (str((sx, sy, sx + cube_size, sy + cube_size)), fn))
|
|
113
|
-
img = imgOut.crop((sx, sy, sx + cube_size, sy + cube_size))
|
|
114
|
-
if rotation[row][col]:
|
|
115
|
-
img = img.rotate(rotation[row][col])
|
|
116
|
-
img.save(fn)
|