@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.
- package/dist/2d/components/rendering/i-camera-2d.component.d.ts +5 -0
- package/dist/2d/components/rendering/i-visual-scene-2d.component.d.ts +1 -2
- package/dist/2d/entities/renderer-2d.entity.d.ts +2 -1
- package/dist/2d/gg-2d-world.d.ts +5 -1
- package/dist/2d/gg-2d-world.js +57 -2
- package/dist/2d/index.d.ts +3 -0
- package/dist/2d/index.js +3 -0
- package/dist/2d/level-loader.d.ts +101 -0
- package/dist/2d/level-loader.js +88 -0
- package/dist/2d/loader.d.ts +11 -0
- package/dist/2d/loader.js +10 -0
- package/dist/3d/components/rendering/{i-camera.component.d.ts → i-camera-3d.component.d.ts} +1 -1
- package/dist/3d/components/rendering/i-camera-3d.component.js +1 -0
- package/dist/3d/components/rendering/i-renderer-3d.component.d.ts +0 -1
- package/dist/3d/components/rendering/i-visual-scene-3d.component.d.ts +1 -2
- package/dist/3d/entities/camera-3d.entity.d.ts +24 -0
- package/dist/3d/entities/camera-3d.entity.js +35 -0
- package/dist/3d/entities/renderer-3d.entity.d.ts +0 -5
- package/dist/3d/entities/renderer-3d.entity.js +0 -15
- package/dist/3d/gg-3d-world.d.ts +2 -2
- package/dist/3d/gg-3d-world.js +73 -0
- package/dist/3d/index.d.ts +3 -1
- package/dist/3d/index.js +3 -1
- package/dist/3d/level-loader.d.ts +300 -0
- package/dist/3d/level-loader.js +267 -0
- package/dist/3d/loader.d.ts +44 -3
- package/dist/3d/loader.js +41 -4
- package/dist/3d/models/gg-meta.d.ts +10 -0
- package/dist/3d/models/gg-meta.js +2 -1
- package/dist/base/blueprint/blueprint-node.d.ts +75 -0
- package/dist/base/blueprint/blueprint-node.js +51 -0
- package/dist/base/blueprint/blueprint.d.ts +123 -0
- package/dist/base/blueprint/blueprint.js +86 -0
- package/dist/base/blueprint/nodes/remove-entity.node.d.ts +26 -0
- package/dist/base/blueprint/nodes/remove-entity.node.js +29 -0
- package/dist/base/clock/pausable-clock.d.ts +12 -0
- package/dist/base/clock/pausable-clock.js +24 -0
- package/dist/base/components/rendering/i-renderer.component.d.ts +1 -0
- package/dist/base/components/rendering/i-visual-scene.component.d.ts +2 -0
- package/dist/base/entities/group.entity.d.ts +17 -0
- package/dist/base/entities/group.entity.js +19 -0
- package/dist/base/entities/i-entity.d.ts +11 -0
- package/dist/base/entities/i-entity.js +28 -0
- package/dist/base/entities/i-renderer.entity.d.ts +7 -1
- package/dist/base/entities/i-renderer.entity.js +15 -0
- package/dist/base/gg-world.d.ts +13 -0
- package/dist/base/gg-world.js +83 -1
- package/dist/base/index.d.ts +5 -0
- package/dist/base/index.js +5 -0
- package/dist/base/inputs/mouse.input.js +1 -0
- package/dist/base/level-loader.d.ts +217 -0
- package/dist/base/level-loader.js +258 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -9
- package/tsconfig.json +4 -1
- package/blender_exporter/build_blender_scene.py +0 -179
- package/blender_exporter/spherical_to_cube_texture.py +0 -116
- /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.
|
|
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
|
@@ -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)
|
|
File without changes
|