@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.
@@ -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) {
@@ -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
- export {};
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
  */
@@ -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.59";
1
+ export declare const VERSION = "0.0.60";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.0.59';
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.59",
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.1",
31
+ "rxjs": "7.8.2",
32
32
  "stats.js": "0.17.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/jest": "^29.5.12",
36
- "@types/stats.js": "^0.17.3",
37
- "jest": "^29.7.0",
38
- "jest-environment-jsdom": "^29.7.0",
39
- "prettier": "^3.3.3",
40
- "ts-jest": "^29.2.4",
41
- "typescript": "~5.5.4"
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)