@gg-web-engine/core 0.0.58 → 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.
Files changed (59) hide show
  1. package/dist/2d/components/rendering/i-camera-2d.component.d.ts +5 -0
  2. package/dist/2d/components/rendering/i-visual-scene-2d.component.d.ts +1 -2
  3. package/dist/2d/entities/renderer-2d.entity.d.ts +2 -1
  4. package/dist/2d/gg-2d-world.d.ts +5 -1
  5. package/dist/2d/gg-2d-world.js +57 -2
  6. package/dist/2d/index.d.ts +3 -0
  7. package/dist/2d/index.js +3 -0
  8. package/dist/2d/level-loader.d.ts +101 -0
  9. package/dist/2d/level-loader.js +88 -0
  10. package/dist/2d/loader.d.ts +11 -0
  11. package/dist/2d/loader.js +10 -0
  12. package/dist/3d/components/rendering/{i-camera.component.d.ts → i-camera-3d.component.d.ts} +1 -1
  13. package/dist/3d/components/rendering/i-camera-3d.component.js +1 -0
  14. package/dist/3d/components/rendering/i-renderer-3d.component.d.ts +0 -1
  15. package/dist/3d/components/rendering/i-visual-scene-3d.component.d.ts +1 -2
  16. package/dist/3d/entities/camera-3d.entity.d.ts +24 -0
  17. package/dist/3d/entities/camera-3d.entity.js +35 -0
  18. package/dist/3d/entities/renderer-3d.entity.d.ts +0 -5
  19. package/dist/3d/entities/renderer-3d.entity.js +0 -15
  20. package/dist/3d/gg-3d-world.d.ts +2 -2
  21. package/dist/3d/gg-3d-world.js +73 -0
  22. package/dist/3d/index.d.ts +3 -1
  23. package/dist/3d/index.js +3 -1
  24. package/dist/3d/level-loader.d.ts +300 -0
  25. package/dist/3d/level-loader.js +267 -0
  26. package/dist/3d/loader.d.ts +44 -3
  27. package/dist/3d/loader.js +41 -4
  28. package/dist/3d/models/gg-meta.d.ts +10 -0
  29. package/dist/3d/models/gg-meta.js +2 -1
  30. package/dist/base/blueprint/blueprint-node.d.ts +75 -0
  31. package/dist/base/blueprint/blueprint-node.js +51 -0
  32. package/dist/base/blueprint/blueprint.d.ts +123 -0
  33. package/dist/base/blueprint/blueprint.js +86 -0
  34. package/dist/base/blueprint/nodes/remove-entity.node.d.ts +26 -0
  35. package/dist/base/blueprint/nodes/remove-entity.node.js +29 -0
  36. package/dist/base/clock/pausable-clock.d.ts +12 -0
  37. package/dist/base/clock/pausable-clock.js +24 -0
  38. package/dist/base/components/rendering/i-renderer.component.d.ts +1 -0
  39. package/dist/base/components/rendering/i-visual-scene.component.d.ts +2 -0
  40. package/dist/base/entities/group.entity.d.ts +17 -0
  41. package/dist/base/entities/group.entity.js +19 -0
  42. package/dist/base/entities/i-entity.d.ts +11 -0
  43. package/dist/base/entities/i-entity.js +28 -0
  44. package/dist/base/entities/i-renderer.entity.d.ts +7 -1
  45. package/dist/base/entities/i-renderer.entity.js +15 -0
  46. package/dist/base/gg-world.d.ts +13 -0
  47. package/dist/base/gg-world.js +83 -1
  48. package/dist/base/index.d.ts +5 -0
  49. package/dist/base/index.js +5 -0
  50. package/dist/base/inputs/mouse.input.js +1 -0
  51. package/dist/base/level-loader.d.ts +217 -0
  52. package/dist/base/level-loader.js +258 -0
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +10 -9
  56. package/tsconfig.json +4 -1
  57. package/blender_exporter/build_blender_scene.py +0 -179
  58. package/blender_exporter/spherical_to_cube_texture.py +0 -116
  59. /package/dist/{3d/components/rendering/i-camera.component.js → 2d/components/rendering/i-camera-2d.component.js} +0 -0
@@ -0,0 +1,258 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { GroupEntity } from './entities/group.entity';
11
+ import { IEntity, TickOrder } from './entities/i-entity';
12
+ import { Blueprint } from './blueprint/blueprint';
13
+ import { RemoveEntityBlueprintNode } from './blueprint/nodes/remove-entity.node';
14
+ /**
15
+ * Base class for level loaders: parses a {@link LevelJson} document into world entities by
16
+ * dispatching each `EntityJson.class` to a generator function registered with {@link registerClass}.
17
+ *
18
+ * A generator is required to return an `IEntity`. Every `IEntity` a generator produces is parented
19
+ * under one {@link GroupEntity} per `loadLevel`/`loadLevelFromUrl` call (added to the world
20
+ * immediately, and handed back once loading completes) - so a whole level can be torn down in one
21
+ * shot with `world.removeEntity(level, true)`, which cascades removal/disposal to every child, and
22
+ * any named entity can be found afterwards with `level.getChildEntityByName(name)`. If a generator
23
+ * returns anything other than an `IEntity` (including `null`/`undefined`), `loadLevel` logs a
24
+ * `console.warn` and skips that entity - it's never parented, named, or tracked.
25
+ * @template D - The position type
26
+ * @template R - The rotation type
27
+ * @template TypeDoc - The type document repository
28
+ */
29
+ export class LevelLoader {
30
+ /**
31
+ * Constructor
32
+ * @param world - The world instance
33
+ */
34
+ constructor(world) {
35
+ this.world = world;
36
+ /**
37
+ * Map of class aliases to generator functions
38
+ */
39
+ this.generators = new Map();
40
+ /**
41
+ * Map of blueprint node type aliases to node factory functions - see {@link registerBlueprintNode}.
42
+ */
43
+ this.blueprintNodes = new Map();
44
+ /**
45
+ * Map of blueprint node type aliases to their default input pin name, for node types registered
46
+ * with one - see {@link registerBlueprintNode}.
47
+ */
48
+ this.blueprintNodeDefaultInputs = new Map();
49
+ this.registerBlueprintNode('RemoveEntity', (w, settings) => new RemoveEntityBlueprintNode(w, settings), 'entity');
50
+ }
51
+ /**
52
+ * Register a generator function for a class alias
53
+ * @param classAlias - The class alias
54
+ * @param generator - The generator function
55
+ */
56
+ registerClass(classAlias, generator) {
57
+ this.generators.set(classAlias, generator);
58
+ }
59
+ /**
60
+ * Register a {@link BlueprintNode} factory for a node type alias, so a `BlueprintJson`'s
61
+ * `nodes` can reference it by `type` (e.g. the built-in `"RemoveEntity"`, registered by every
62
+ * `LevelLoader` out of the box). Same pattern as {@link registerClass}, one level down (node
63
+ * types within a blueprint graph, rather than entity classes within a level).
64
+ * @param typeAlias - The node type alias
65
+ * @param factory - Builds a node instance from its baked-in settings
66
+ * @param defaultInputPin - This node type's sole "trigger me" input pin name, if it has one
67
+ * canonical one (e.g. `"RemoveEntity"`'s `"entity"`). Enables the node type to be used directly
68
+ * as an `EntityJson.events` binding (`{ "eventName": "TypeAlias" }` or
69
+ * `{ "eventName": { "type": "TypeAlias", "settings": {...} } }`) without declaring a full
70
+ * `BlueprintJson` graph in `blueprints` - see {@link EntityEventBinding}. Omit for a node type
71
+ * with zero or multiple input pins, or one with no single obviously-correct default; it remains
72
+ * usable from a full graph either way.
73
+ */
74
+ registerBlueprintNode(typeAlias, factory, defaultInputPin) {
75
+ this.blueprintNodes.set(typeAlias, factory);
76
+ if (defaultInputPin !== undefined) {
77
+ this.blueprintNodeDefaultInputs.set(typeAlias, defaultInputPin);
78
+ }
79
+ else {
80
+ this.blueprintNodeDefaultInputs.delete(typeAlias);
81
+ }
82
+ }
83
+ /**
84
+ * Load a level from an already-parsed JSON document. Every `IEntity` the level's entities
85
+ * produce is parented under - and, on failure, torn down along with - the returned
86
+ * {@link GroupEntity}, already added to the world.
87
+ * @param levelJson - The level JSON
88
+ * @param levelName - Optional name for the returned group entity (e.g. so a debugger/console
89
+ * listing entities by name shows something more meaningful than the default auto-generated one)
90
+ * @returns The level's root group entity
91
+ */
92
+ loadLevel(levelJson, levelName) {
93
+ return __awaiter(this, void 0, void 0, function* () {
94
+ const level = new GroupEntity();
95
+ if (levelName !== undefined) {
96
+ level.name = levelName;
97
+ }
98
+ this.world.addEntity(level);
99
+ try {
100
+ for (const entityJson of levelJson.entities) {
101
+ const { class: classAlias, shape, position, rotation, name, config, events } = entityJson;
102
+ const generator = this.generators.get(classAlias);
103
+ if (!generator) {
104
+ console.warn(`No generator registered for class alias "${classAlias}"`);
105
+ continue;
106
+ }
107
+ const settings = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (config !== null && config !== void 0 ? config : {})), (shape !== undefined ? { shape } : {})), (position !== undefined ? { position } : {})), (rotation !== undefined ? { rotation } : {})), (name !== undefined ? { name } : {}));
108
+ const entity = yield generator(this.world, settings);
109
+ if (!(entity instanceof IEntity)) {
110
+ console.warn(`Generator for class alias "${classAlias}" did not return an IEntity - skipping`);
111
+ continue;
112
+ }
113
+ if (name !== undefined) {
114
+ entity.name = name;
115
+ }
116
+ // addChildren reparents the entity under level regardless of whether a generator already
117
+ // self-added it to the world (e.g. addPrimitiveRigidBody does) - safe either way.
118
+ level.addChildren(entity);
119
+ if (events) {
120
+ for (const [eventName, eventBinding] of Object.entries(events)) {
121
+ const bindingEntity = this.bindEvent(entity, eventName, eventBinding, levelJson.blueprints);
122
+ if (bindingEntity) {
123
+ level.addChildren(bindingEntity);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ catch (e) {
130
+ // Don't leave a partially-loaded level (and its already-spawned entities) behind if a
131
+ // generator throws partway through - the caller never gets `level` back to clean it up itself.
132
+ this.world.removeEntity(level, true);
133
+ throw e;
134
+ }
135
+ return level;
136
+ });
137
+ }
138
+ /**
139
+ * Resolve `eventBinding` (see {@link EntityEventBinding}) to a `BlueprintJson`, instantiate a
140
+ * fresh `Blueprint` from it, and subscribe it to `entity[eventName]` so every value that
141
+ * observable emits triggers the blueprint's `"in"` entry point. Wrapped in a
142
+ * `BlueprintBindingEntity` so the subscription (and the blueprint's own node state) is torn down
143
+ * automatically once that entity is disposed - the caller parents the returned entity under the
144
+ * level's group for that reason.
145
+ * @param entity - The entity carrying the observable property
146
+ * @param eventName - Name of the observable property on `entity`
147
+ * @param eventBinding - What to run - a `blueprints` name, a bare node type alias, or `{ type,
148
+ * settings? }`
149
+ * @param blueprints - The level's top-level blueprint map, if any
150
+ * @returns The binding entity to parent under the level, or `undefined` if `eventBinding`
151
+ * couldn't be resolved or the named property isn't an `Observable` (both logged via
152
+ * `console.warn`)
153
+ */
154
+ bindEvent(entity, eventName, eventBinding, blueprints) {
155
+ const blueprintJson = this.resolveEventBlueprint(eventName, eventBinding, blueprints);
156
+ if (!blueprintJson) {
157
+ return undefined;
158
+ }
159
+ const observable = entity[eventName];
160
+ if (!observable || typeof observable.subscribe !== 'function') {
161
+ console.warn(`Entity has no observable property "${eventName}" to bind a blueprint to`);
162
+ return undefined;
163
+ }
164
+ const blueprint = new Blueprint(this.world, blueprintJson, this.blueprintNodes);
165
+ return new BlueprintBindingEntity(blueprint, observable);
166
+ }
167
+ /**
168
+ * Turn an `EntityEventBinding` into a `BlueprintJson` to run. An object form (`{ type,
169
+ * settings? }`) always builds a single-node inline graph via {@link inlineNodeBlueprint}. A
170
+ * string form is tried first as a key into `blueprints` (a named, possibly multi-node graph),
171
+ * then - if not found there - as a bare node type alias, same as the object form with no
172
+ * settings.
173
+ * @param eventName - Name of the observable property being bound, for warning messages
174
+ * @param eventBinding - The binding to resolve
175
+ * @param blueprints - The level's top-level blueprint map, if any
176
+ * @returns The resolved graph, or `undefined` (logged via `console.warn`) if it couldn't be
177
+ */
178
+ resolveEventBlueprint(eventName, eventBinding, blueprints) {
179
+ if (typeof eventBinding === 'object') {
180
+ return this.inlineNodeBlueprint(eventName, eventBinding.type, eventBinding.settings);
181
+ }
182
+ const named = blueprints === null || blueprints === void 0 ? void 0 : blueprints[eventBinding];
183
+ if (named) {
184
+ return named;
185
+ }
186
+ if (this.blueprintNodes.has(eventBinding)) {
187
+ return this.inlineNodeBlueprint(eventName, eventBinding, undefined);
188
+ }
189
+ console.warn(`No blueprint or blueprint node type named "${eventBinding}" found for event "${eventName}" - skipping`);
190
+ return undefined;
191
+ }
192
+ /**
193
+ * Build a single-node `BlueprintJson` wrapping one blueprint node type, wired so the node's
194
+ * registered default input pin (see {@link registerBlueprintNode}) is reachable as `"in"` - what
195
+ * powers the `EntityEventBinding` shorthand that skips declaring a `blueprints` entry entirely.
196
+ * @param eventName - Name of the observable property being bound, for warning messages
197
+ * @param nodeType - The blueprint node type alias
198
+ * @param settings - Settings to bake into the node, if any
199
+ * @returns The single-node graph, or `undefined` (logged via `console.warn`) if `nodeType` isn't
200
+ * registered, or was registered without a default input pin
201
+ */
202
+ inlineNodeBlueprint(eventName, nodeType, settings) {
203
+ if (!this.blueprintNodes.has(nodeType)) {
204
+ console.warn(`No blueprint node type registered for "${nodeType}" (event "${eventName}") - skipping`);
205
+ return undefined;
206
+ }
207
+ const inputPin = this.blueprintNodeDefaultInputs.get(nodeType);
208
+ if (!inputPin) {
209
+ console.warn(`Blueprint node type "${nodeType}" has no default input pin registered - event "${eventName}" must ` +
210
+ `reference a full graph declared in "blueprints" instead, addressing the desired pin explicitly`);
211
+ return undefined;
212
+ }
213
+ return {
214
+ nodes: [{ id: 'n1', type: nodeType, settings }],
215
+ inputs: { in: { node: 'n1', pin: inputPin } },
216
+ };
217
+ }
218
+ /**
219
+ * Fetch a level JSON document hosted at `url` and load it, so a whole level/scene can be
220
+ * shipped and consumed as a single static JSON file.
221
+ * @param url - URL (or path) of the level JSON document
222
+ * @param levelName - Optional name for the returned group entity, see {@link loadLevel}
223
+ * @returns The level's root group entity
224
+ */
225
+ loadLevelFromUrl(url, levelName) {
226
+ return __awaiter(this, void 0, void 0, function* () {
227
+ const response = yield fetch(url);
228
+ if (!response.ok) {
229
+ throw new Error(`Failed to load level JSON from "${url}": ${response.status} ${response.statusText}`);
230
+ }
231
+ const levelJson = yield response.json();
232
+ return this.loadLevel(levelJson, levelName);
233
+ });
234
+ }
235
+ }
236
+ /**
237
+ * Plain do-nothing `IEntity` that owns one event-to-blueprint binding created by
238
+ * `LevelLoader.bindEvent`: subscribes to the bound observable on construction, and unsubscribes
239
+ * plus disposes the `Blueprint` on `dispose()`. Parented under the level's group entity like any
240
+ * other level-produced entity, so `world.removeEntity(level, true)` tears the binding down along
241
+ * with the rest of the level - there is nothing else app code needs to do to clean it up.
242
+ * @template D - The position type
243
+ * @template R - The rotation type
244
+ * @template TypeDoc - The type document repository
245
+ */
246
+ class BlueprintBindingEntity extends IEntity {
247
+ constructor(blueprint, observable) {
248
+ super();
249
+ this.blueprint = blueprint;
250
+ this.tickOrder = TickOrder.CONTROLLERS;
251
+ this.subscription = observable.subscribe(value => this.blueprint.trigger('in', value));
252
+ }
253
+ dispose() {
254
+ this.subscription.unsubscribe();
255
+ this.blueprint.dispose();
256
+ super.dispose();
257
+ }
258
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.58";
1
+ export declare const VERSION = "0.0.60";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.0.58';
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.58",
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
@@ -2,7 +2,10 @@
2
2
  "extends": "./../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "baseUrl": "./src/",
5
- "outDir": "./dist/"
5
+ "outDir": "./dist/",
6
+ "rootDir": "./src/",
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. */
6
9
  },
7
10
  "include": ["*.ts", "**/*.ts"],
8
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)