babylon.quarks 0.26.0 → 0.28.0
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/CHANGELOG.md +1194 -8
- package/README.md +13 -9
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.cts +10108 -0
- package/dist/types/index.d.ts +5794 -998
- package/package.json +18 -7
- package/src/BatchedRenderer.ts +274 -156
- package/src/EffectPool.ts +94 -12
- package/src/MeshSurfaceEmitter.ts +50 -14
- package/src/ParticleEmitter.ts +75 -17
- package/src/ParticleSystem.ts +981 -2066
- package/src/QuarksLoader.ts +666 -194
- package/src/QuarksPrefab.ts +367 -73
- package/src/QuarksSerializer.ts +512 -0
- package/src/QuarksUtil.ts +77 -7
- package/src/SpriteBatch.ts +127 -252
- package/src/TrailBatch.ts +545 -348
- package/src/VFXBatch.ts +238 -596
- package/src/batchKey.ts +239 -0
- package/src/batchSettings.ts +546 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +76 -0
- package/src/envAtlas.ts +126 -18
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +69 -35
- package/src/nodeId.ts +36 -0
- package/src/shaderProgram.ts +419 -0
- package/src/shaderVariants.ts +245 -0
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +12 -5
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +85 -0
- package/src/shaders/chunks/surface.ts +12 -2
- package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
- package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
- package/src/shaders/particle_frag.glsl.ts +5 -22
- package/src/shaders/particle_frag.wgsl.ts +5 -23
- package/src/shaders/particle_physics_frag.glsl.ts +16 -26
- package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
- package/src/shaders/particle_vert.glsl.ts +5 -27
- package/src/shaders/particle_vert.wgsl.ts +5 -26
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
- package/src/shaders/trail_frag.glsl.ts +13 -0
- package/src/shaders/trail_frag.wgsl.ts +13 -0
- package/src/shaders/trail_vert.glsl.ts +45 -36
- package/src/shaders/trail_vert.wgsl.ts +46 -40
- package/src/softDepth.ts +136 -0
- package/src/system/BehaviorPlan.ts +85 -0
- package/src/system/BoundsTracker.ts +181 -0
- package/src/system/EmissionController.ts +358 -0
- package/src/system/EventDispatcher.ts +60 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +104 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +165 -0
- package/src/system/normalize.ts +100 -0
- package/src/system/serialization.ts +422 -0
- package/src/system/settingsSchema.ts +339 -0
- package/src/system/types.ts +449 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +54 -0
- package/src/BatchedParticleRenderer.ts +0 -3
- package/src/materials/ParticleMaterials.ts +0 -23
- package/src/materials/index.ts +0 -1
- package/src/shaders/chunks/index.ts +0 -1
- package/src/shaders/chunks/register-shader-chunks.ts +0 -32
- package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
- package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
- package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
- package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
- package/src/shaders/index.ts +0 -9
- package/src/shaders/local_particle_vert.glsl.ts +0 -50
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
|
|
2
|
+
import {Constants} from '@babylonjs/core/Engines/constants';
|
|
3
|
+
import {StandardMaterial} from '@babylonjs/core/Materials/standardMaterial';
|
|
4
|
+
import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
5
|
+
import {Texture} from '@babylonjs/core/Materials/Textures/texture';
|
|
6
|
+
import {Matrix, Quaternion} from '@babylonjs/core/Maths/math.vector';
|
|
7
|
+
import {Geometry} from '@babylonjs/core/Meshes/geometry';
|
|
8
|
+
import {InstancedMesh} from '@babylonjs/core/Meshes/instancedMesh';
|
|
9
|
+
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
10
|
+
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
11
|
+
import type {GeometryEntry, QuarksMaterialJSON} from './effectDocumentJSON';
|
|
12
|
+
import {ensureTriangleIndices} from './geometryUtil';
|
|
13
|
+
import {quarksNodeId} from './nodeId';
|
|
14
|
+
import {ParticleEmitter} from './ParticleEmitter';
|
|
15
|
+
import type {BabylonMetaData, ParticleSystem, ParticleSystemJSONParameters} from './ParticleSystem';
|
|
16
|
+
import type {AnimationJSON} from './QuarksPrefab';
|
|
17
|
+
import {QuarksPrefab} from './QuarksPrefab';
|
|
18
|
+
import {textureSourceUrl} from './textureSource';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The version of the effect file format this library writes, in `metadata.quarksFormat`. It goes
|
|
22
|
+
* up when a release writes something an older loader would read wrongly. `QuarksLoader`
|
|
23
|
+
* reads every earlier version, and files written before the field existed, and warns about a
|
|
24
|
+
* file from a newer version.
|
|
25
|
+
*/
|
|
26
|
+
export const QUARKS_FORMAT_VERSION = 1;
|
|
27
|
+
|
|
28
|
+
/** An image a texture reads: a url, a root-relative path, or a `data:` URI. */
|
|
29
|
+
export interface QuarksImageJSON {
|
|
30
|
+
/** Unique within the file; other entries refer to it by this. */
|
|
31
|
+
uuid: string;
|
|
32
|
+
/** Relative to the file's base url, absolute, or a `data:` url. */
|
|
33
|
+
url: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A texture and its sampler state. Every field after `image` is written only when it differs from
|
|
38
|
+
* Babylon's default for a `Texture`. `wrap` uses the three.js codes the format inherited: 1000
|
|
39
|
+
* repeat, 1001 clamp, 1002 mirror.
|
|
40
|
+
*/
|
|
41
|
+
export interface QuarksTextureJSON {
|
|
42
|
+
/** Unique within the file; other entries refer to it by this. */
|
|
43
|
+
uuid: string;
|
|
44
|
+
/** The {@link QuarksImageJSON} uuid; absent when the texture had no url a file can point at. */
|
|
45
|
+
image?: string;
|
|
46
|
+
/** Default true. */
|
|
47
|
+
invertY?: boolean;
|
|
48
|
+
/** Default false. */
|
|
49
|
+
noMipmap?: boolean;
|
|
50
|
+
/** A Babylon sampling mode constant. Default trilinear. */
|
|
51
|
+
samplingMode?: number;
|
|
52
|
+
/** `[u, v]`. Default repeat on both. */
|
|
53
|
+
wrap?: [number, number];
|
|
54
|
+
/** `[uScale, vScale]`. Default `[1, 1]`. */
|
|
55
|
+
repeat?: [number, number];
|
|
56
|
+
/** `[uOffset, vOffset]`. Default `[0, 0]`. */
|
|
57
|
+
offset?: [number, number];
|
|
58
|
+
/** `wAng`, in radians. Default 0. */
|
|
59
|
+
rotation?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Mesh data, inline. */
|
|
63
|
+
export interface QuarksGeometryJSON {
|
|
64
|
+
/** Unique within the file; other entries refer to it by this. */
|
|
65
|
+
uuid: string;
|
|
66
|
+
/** Always `'QuarksGeometry'`. */
|
|
67
|
+
type: 'QuarksGeometry';
|
|
68
|
+
/** `x, y, z` per vertex. */
|
|
69
|
+
positions: number[];
|
|
70
|
+
/** Three per triangle. */
|
|
71
|
+
indices: number[];
|
|
72
|
+
/** First UV set, `u, v` per vertex. */
|
|
73
|
+
uvs?: number[];
|
|
74
|
+
/** Second UV set, `u, v` per vertex. */
|
|
75
|
+
uvs2?: number[];
|
|
76
|
+
/** `x, y, z` per vertex. */
|
|
77
|
+
normals?: number[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A node of an effect: a group, an emitter, a mesh or a prefab, with its children. */
|
|
81
|
+
export interface QuarksNodeJSON {
|
|
82
|
+
/** Unique within the file; other entries refer to it by this. */
|
|
83
|
+
uuid: string;
|
|
84
|
+
/** What the node is; a type the loader does not know loads as an empty node. */
|
|
85
|
+
type: 'Group' | 'ParticleEmitter' | 'Mesh' | 'QuarksPrefab' | string;
|
|
86
|
+
/** The node's name. */
|
|
87
|
+
name?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Local transform, column-major, as Babylon's `Matrix.asArray()`. Always written; a three.js
|
|
90
|
+
* file without it places the node by `position`, `quaternion` or `rotation`, and `scale`.
|
|
91
|
+
*/
|
|
92
|
+
matrix?: number[];
|
|
93
|
+
/** Written only when the node is disabled (false); loads it disabled, with everything under it. */
|
|
94
|
+
visible?: boolean;
|
|
95
|
+
/** `Mesh`: written only when the mesh is not drawn (`isVisible`), which leaves its children drawn. */
|
|
96
|
+
isVisible?: false;
|
|
97
|
+
/** Child nodes, in order. */
|
|
98
|
+
children?: QuarksNodeJSON[];
|
|
99
|
+
/** `ParticleEmitter`: the system. */
|
|
100
|
+
ps?: ParticleSystemJSONParameters;
|
|
101
|
+
/** `Mesh`: the {@link QuarksGeometryJSON} uuid. */
|
|
102
|
+
geometry?: string;
|
|
103
|
+
/** `Mesh`: the material uuid; a three.js mesh may list several, of which the first is used. */
|
|
104
|
+
material?: string | string[];
|
|
105
|
+
/** `QuarksPrefab`: the timeline's speed, written only when not 1. */
|
|
106
|
+
timeScale?: number;
|
|
107
|
+
/** `QuarksPrefab`: the timeline's tracks. */
|
|
108
|
+
animationData?: AnimationJSON[];
|
|
109
|
+
/** three.js files without `matrix`: the position as `[x, y, z]`. */
|
|
110
|
+
position?: number[];
|
|
111
|
+
/** three.js files without `matrix`: the scale as `[x, y, z]`. */
|
|
112
|
+
scale?: number[];
|
|
113
|
+
/** three.js files without `matrix`: the rotation as a quaternion `[x, y, z, w]`. */
|
|
114
|
+
quaternion?: number[];
|
|
115
|
+
/** three.js files without `matrix` or `quaternion`: Euler angles `[x, y, z]` in radians. */
|
|
116
|
+
rotation?: number[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A whole effect, as {@link serializeEffect} writes it and `QuarksLoader` reads it. */
|
|
120
|
+
export interface QuarksEffectJSON {
|
|
121
|
+
/** What wrote the file, and which format version it follows. */
|
|
122
|
+
metadata: {
|
|
123
|
+
/** The three.js object format version the envelope follows. */
|
|
124
|
+
version: number;
|
|
125
|
+
/** Always `'Object3D'`. */
|
|
126
|
+
type: 'Object3D';
|
|
127
|
+
/** The tool that wrote the file, from {@link SerializeEffectOptions.generator}. */
|
|
128
|
+
generator: string;
|
|
129
|
+
/** See {@link QUARKS_FORMAT_VERSION}. */
|
|
130
|
+
quarksFormat: number;
|
|
131
|
+
};
|
|
132
|
+
/** Meshes the nodes and particle systems use. */
|
|
133
|
+
geometries: QuarksGeometryJSON[];
|
|
134
|
+
/** Materials the particle systems and `Mesh` nodes use, referred to by uuid. */
|
|
135
|
+
materials: QuarksMaterialJSON[];
|
|
136
|
+
/** Textures the particle systems use. */
|
|
137
|
+
textures: QuarksTextureJSON[];
|
|
138
|
+
/** Image files the textures load. */
|
|
139
|
+
images: QuarksImageJSON[];
|
|
140
|
+
/** The effect's root node. */
|
|
141
|
+
object: QuarksNodeJSON;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Options for {@link serializeEffect}. */
|
|
145
|
+
export interface SerializeEffectOptions {
|
|
146
|
+
/** Written to `metadata.generator`. Default `'babylon.quarks'`. */
|
|
147
|
+
generator?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Leaves a node and everything under it out of the file when it returns false, e.g. helpers an
|
|
150
|
+
* editor parents under an effect.
|
|
151
|
+
*/
|
|
152
|
+
filter?: (node: TransformNode) => boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Called for each part of the effect the file cannot hold as it is, such as a texture with no
|
|
155
|
+
* url a file can point at. Default: `console.warn`.
|
|
156
|
+
*/
|
|
157
|
+
onWarning?: (message: string) => void;
|
|
158
|
+
/** Several roots are written as children of one group; its name. Default `'effect'`. */
|
|
159
|
+
name?: string;
|
|
160
|
+
/**
|
|
161
|
+
* The url written for a texture's image. Default: the url the texture's image had in the file
|
|
162
|
+
* it was loaded from (see {@link textureSourceUrl}), so a file saved next to its source loads
|
|
163
|
+
* the same images; for a texture made in code, its `url`. Return a path relative to where the
|
|
164
|
+
* file will be saved when that differs from the source, or give a texture made in code a url or
|
|
165
|
+
* a `data:` URI. Return `null` to write the texture without an image.
|
|
166
|
+
*/
|
|
167
|
+
imageUrl?: (texture: Texture) => string | null | undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** three.js wrap codes, as the loader reads them. */
|
|
171
|
+
const WRAP_TO_JSON: {[mode: number]: number} = {
|
|
172
|
+
[Texture.WRAP_ADDRESSMODE]: 1000,
|
|
173
|
+
[Texture.CLAMP_ADDRESSMODE]: 1001,
|
|
174
|
+
[Texture.MIRROR_ADDRESSMODE]: 1002,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Whether a file can name `url` and get the image back elsewhere. A `blob:` url lives only in the
|
|
179
|
+
* page that made it, and Babylon's glTF loader names embedded images with a `data:` placeholder
|
|
180
|
+
* that has no payload.
|
|
181
|
+
*/
|
|
182
|
+
function isPortableUrl(url: string | null | undefined): url is string {
|
|
183
|
+
if (!url) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (/^blob:/i.test(url)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return !/^data:/i.test(url) || url.includes(',');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function localMatrix(node: TransformNode): number[] {
|
|
193
|
+
const rotation = node.rotationQuaternion ?? Quaternion.FromEulerVector(node.rotation);
|
|
194
|
+
return Array.from(Matrix.Compose(node.scaling, rotation, node.position).asArray());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
class EffectWriter {
|
|
198
|
+
readonly meta: BabylonMetaData = {geometries: {}, materials: {}, textures: {}};
|
|
199
|
+
private readonly meshGeometries = new Map<string, QuarksGeometryJSON>();
|
|
200
|
+
private readonly geometryIds = new Map<Geometry, string>();
|
|
201
|
+
private readonly meshMaterials: Array<{json: QuarksMaterialJSON; map: BaseTexture | null}> = [];
|
|
202
|
+
private readonly meshMaterialIds = new Map<StandardMaterial, string>();
|
|
203
|
+
private readonly extraTextures = new Map<BaseTexture, string>();
|
|
204
|
+
private nextId = 0;
|
|
205
|
+
|
|
206
|
+
constructor(private readonly options: SerializeEffectOptions) {}
|
|
207
|
+
|
|
208
|
+
/** Reports links to nodes the file does not hold: they load unresolved. */
|
|
209
|
+
private warnDanglingLinks(object: QuarksNodeJSON): void {
|
|
210
|
+
const written = new Set<string>();
|
|
211
|
+
const nodes: QuarksNodeJSON[] = [];
|
|
212
|
+
const walk = (node: QuarksNodeJSON) => {
|
|
213
|
+
written.add(node.uuid);
|
|
214
|
+
nodes.push(node);
|
|
215
|
+
node.children?.forEach(walk);
|
|
216
|
+
};
|
|
217
|
+
walk(object);
|
|
218
|
+
for (const node of nodes) {
|
|
219
|
+
for (const [kind, target] of links(node)) {
|
|
220
|
+
if (target !== '' && !written.has(target)) {
|
|
221
|
+
this.warn(
|
|
222
|
+
`"${node.name}" has ${kind} on node "${target}", which is not in what is written; ` +
|
|
223
|
+
'it loads without that link.'
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
warn(message: string): void {
|
|
231
|
+
(this.options.onWarning ?? ((m: string) => console.warn(`[quarks] ${m}`)))(message);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
node(node: TransformNode): QuarksNodeJSON {
|
|
235
|
+
const children = node
|
|
236
|
+
.getChildren((child) => child instanceof TransformNode, true)
|
|
237
|
+
.filter((child) => this.options.filter?.(child as TransformNode) !== false)
|
|
238
|
+
.map((child) => this.node(child as TransformNode));
|
|
239
|
+
const json: QuarksNodeJSON = {
|
|
240
|
+
uuid: quarksNodeId(node),
|
|
241
|
+
type: 'Group',
|
|
242
|
+
name: node.name,
|
|
243
|
+
matrix: localMatrix(node),
|
|
244
|
+
};
|
|
245
|
+
if (!node.isEnabled(false)) {
|
|
246
|
+
json.visible = false;
|
|
247
|
+
}
|
|
248
|
+
// A mesh that is not drawn, such as a mesh surface shape's source, may still parent emitters.
|
|
249
|
+
if (node instanceof Mesh && !node.isVisible) {
|
|
250
|
+
json.isVisible = false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (node instanceof ParticleEmitter) {
|
|
254
|
+
json.type = 'ParticleEmitter';
|
|
255
|
+
json.ps = (node.system as ParticleSystem).toJSON(this.meta);
|
|
256
|
+
} else if (node instanceof QuarksPrefab) {
|
|
257
|
+
const {type: _type, name: _name, ...prefab} = node.toJSON();
|
|
258
|
+
Object.assign(json, prefab);
|
|
259
|
+
json.type = 'QuarksPrefab';
|
|
260
|
+
} else if (node instanceof Mesh) {
|
|
261
|
+
json.type = 'Mesh';
|
|
262
|
+
json.geometry = this.meshGeometry(node);
|
|
263
|
+
json.material = this.meshMaterial(node);
|
|
264
|
+
} else if (node instanceof InstancedMesh) {
|
|
265
|
+
this.warn(`"${node.name}" is an instance of "${node.sourceMesh.name}"; it is written as a group.`);
|
|
266
|
+
}
|
|
267
|
+
if (children.length > 0) {
|
|
268
|
+
json.children = children;
|
|
269
|
+
}
|
|
270
|
+
return json;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private id(prefix: string): string {
|
|
274
|
+
this.nextId += 1;
|
|
275
|
+
return `${prefix}_${this.nextId}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private meshGeometry(mesh: Mesh): string | undefined {
|
|
279
|
+
// Meshes sharing one Geometry (clones) share one entry.
|
|
280
|
+
const shared = mesh.geometry ? this.geometryIds.get(mesh.geometry) : undefined;
|
|
281
|
+
if (shared !== undefined) {
|
|
282
|
+
return shared;
|
|
283
|
+
}
|
|
284
|
+
const positions = mesh.getVerticesData(VertexBuffer.PositionKind);
|
|
285
|
+
if (!positions || positions.length === 0) {
|
|
286
|
+
this.warn(`Mesh "${mesh.name}" has no vertices; it is written without geometry.`);
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
// An unindexed mesh draws its vertices in order, as the loader does for a file without indices.
|
|
290
|
+
const indexed = mesh.getIndices();
|
|
291
|
+
const indices = ensureTriangleIndices(
|
|
292
|
+
positions instanceof Float32Array ? positions : new Float32Array(positions),
|
|
293
|
+
indexed && indexed.length > 0 ? Uint32Array.from(indexed) : undefined
|
|
294
|
+
);
|
|
295
|
+
const read = (kind: string) => {
|
|
296
|
+
const data = mesh.getVerticesData(kind);
|
|
297
|
+
return data ? Array.from(data) : undefined;
|
|
298
|
+
};
|
|
299
|
+
const geometry: QuarksGeometryJSON = {
|
|
300
|
+
uuid: this.id('quarks_mesh_geometry'),
|
|
301
|
+
type: 'QuarksGeometry',
|
|
302
|
+
positions: Array.from(positions),
|
|
303
|
+
indices: Array.from(indices),
|
|
304
|
+
};
|
|
305
|
+
const uvs = read(VertexBuffer.UVKind);
|
|
306
|
+
const uvs2 = read(VertexBuffer.UV2Kind);
|
|
307
|
+
const normals = read(VertexBuffer.NormalKind);
|
|
308
|
+
if (uvs) geometry.uvs = uvs;
|
|
309
|
+
if (uvs2) geometry.uvs2 = uvs2;
|
|
310
|
+
if (normals) geometry.normals = normals;
|
|
311
|
+
this.meshGeometries.set(geometry.uuid, geometry);
|
|
312
|
+
if (mesh.geometry) {
|
|
313
|
+
this.geometryIds.set(mesh.geometry, geometry.uuid);
|
|
314
|
+
}
|
|
315
|
+
return geometry.uuid;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* A mesh node's material, as far as the loader rebuilds one: blend, culling, alpha cutoff,
|
|
320
|
+
* depth write and the diffuse texture of a StandardMaterial. A source mesh for a mesh surface
|
|
321
|
+
* shape is usually hidden and needs none.
|
|
322
|
+
*/
|
|
323
|
+
private meshMaterial(mesh: Mesh): string | undefined {
|
|
324
|
+
const material = mesh.material;
|
|
325
|
+
if (!material) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
if (!(material instanceof StandardMaterial)) {
|
|
329
|
+
this.warn(
|
|
330
|
+
`Mesh "${mesh.name}" has a ${material.getClassName()}; only a StandardMaterial's blend, ` +
|
|
331
|
+
'culling and diffuse texture are written, so it loads without a material.'
|
|
332
|
+
);
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
let uuid = this.meshMaterialIds.get(material);
|
|
336
|
+
if (uuid !== undefined) {
|
|
337
|
+
return uuid;
|
|
338
|
+
}
|
|
339
|
+
uuid = this.id('quarks_mesh_material');
|
|
340
|
+
this.meshMaterialIds.set(material, uuid);
|
|
341
|
+
const white = material.diffuseColor.r === 1 && material.diffuseColor.g === 1 && material.diffuseColor.b === 1;
|
|
342
|
+
const black =
|
|
343
|
+
material.emissiveColor.r === 0 && material.emissiveColor.g === 0 && material.emissiveColor.b === 0;
|
|
344
|
+
if (material.alpha !== 1 || !white || !black) {
|
|
345
|
+
this.warn(`Mesh "${mesh.name}": its material's alpha and colours are not written; it loads opaque white.`);
|
|
346
|
+
}
|
|
347
|
+
// Each field only when it differs from what the loader gives a material without it.
|
|
348
|
+
const json: QuarksMaterialJSON = {uuid, type: 'MeshBasicMaterial'};
|
|
349
|
+
if (material.alphaMode !== Constants.ALPHA_COMBINE) json.alphaMode = material.alphaMode;
|
|
350
|
+
if (material.backFaceCulling) json.side = 0;
|
|
351
|
+
if (material.alphaCutOff !== 0) json.alphaTest = material.alphaCutOff;
|
|
352
|
+
if (!material.disableDepthWrite) json.depthWrite = true;
|
|
353
|
+
// The texture's uuid is settled in finish(), once the systems have written theirs.
|
|
354
|
+
this.meshMaterials.push({json, map: material.diffuseTexture});
|
|
355
|
+
return uuid;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The uuid of a texture a mesh node uses: the systems' entry for it, or one of its own. */
|
|
359
|
+
private texture(texture: BaseTexture): string {
|
|
360
|
+
for (const [uuid, known] of Object.entries(this.meta.textures)) {
|
|
361
|
+
if (known === texture) {
|
|
362
|
+
return uuid;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
let uuid = this.extraTextures.get(texture);
|
|
366
|
+
if (uuid === undefined) {
|
|
367
|
+
uuid = this.id('quarks_mesh_texture');
|
|
368
|
+
this.extraTextures.set(texture, uuid);
|
|
369
|
+
}
|
|
370
|
+
return uuid;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
finish(object: QuarksNodeJSON): QuarksEffectJSON {
|
|
374
|
+
const meshMaterials = this.meshMaterials.map(({json, map}) => (map ? {...json, map: this.texture(map)} : json));
|
|
375
|
+
this.warnDanglingLinks(object);
|
|
376
|
+
const images: QuarksImageJSON[] = [];
|
|
377
|
+
const imageIds = new Map<string, string>();
|
|
378
|
+
const textures: QuarksTextureJSON[] = [];
|
|
379
|
+
const entries: Array<[string, BaseTexture | null]> = [
|
|
380
|
+
...Object.entries(this.meta.textures),
|
|
381
|
+
...[...this.extraTextures].map(([texture, uuid]) => [uuid, texture] as [string, BaseTexture]),
|
|
382
|
+
];
|
|
383
|
+
for (const [uuid, texture] of entries) {
|
|
384
|
+
if (texture === null) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const json: QuarksTextureJSON = {uuid};
|
|
388
|
+
const url =
|
|
389
|
+
texture instanceof Texture
|
|
390
|
+
? this.options.imageUrl
|
|
391
|
+
? this.options.imageUrl(texture)
|
|
392
|
+
: (textureSourceUrl(texture) ?? texture.url)
|
|
393
|
+
: null;
|
|
394
|
+
if (isPortableUrl(url)) {
|
|
395
|
+
let image = imageIds.get(url);
|
|
396
|
+
if (image === undefined) {
|
|
397
|
+
image = `quarks_image_${imageIds.size + 1}`;
|
|
398
|
+
imageIds.set(url, image);
|
|
399
|
+
images.push({uuid: image, url});
|
|
400
|
+
}
|
|
401
|
+
json.image = image;
|
|
402
|
+
} else {
|
|
403
|
+
this.warn(
|
|
404
|
+
`Texture "${texture.name}" has no url a file can point at (made in code, a blob: url, ` +
|
|
405
|
+
'or an image embedded in a glTF); it is written without its image and loads as no ' +
|
|
406
|
+
'texture. Give it a file url or a data: URI before saving.'
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
if (texture instanceof Texture) {
|
|
410
|
+
Object.assign(json, samplerJSON(texture));
|
|
411
|
+
}
|
|
412
|
+
textures.push(json);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const materials = Object.values(this.meta.materials).map((material) => {
|
|
416
|
+
// The live Babylon material is for in-memory reuse only.
|
|
417
|
+
const {sourceMaterial: _sourceMaterial, ...serializable} = material;
|
|
418
|
+
return serializable;
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
metadata: {
|
|
423
|
+
version: 4.5,
|
|
424
|
+
type: 'Object3D',
|
|
425
|
+
generator: this.options.generator ?? 'babylon.quarks',
|
|
426
|
+
quarksFormat: QUARKS_FORMAT_VERSION,
|
|
427
|
+
},
|
|
428
|
+
geometries: [
|
|
429
|
+
...Object.values(this.meta.geometries).filter(isQuarksGeometryJSON),
|
|
430
|
+
...this.meshGeometries.values(),
|
|
431
|
+
],
|
|
432
|
+
materials: [...materials, ...meshMaterials],
|
|
433
|
+
textures,
|
|
434
|
+
images,
|
|
435
|
+
object,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Whether a system's geometry entry is inline quarks data, the form its `toJSON` writes. */
|
|
441
|
+
function isQuarksGeometryJSON(entry: GeometryEntry): entry is QuarksGeometryJSON {
|
|
442
|
+
return 'type' in entry && entry.type === 'QuarksGeometry' && Array.isArray(entry.positions);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Ids a written document links to by uuid, by the kind of link. */
|
|
446
|
+
function* links(node: QuarksNodeJSON): Generator<[string, string]> {
|
|
447
|
+
const ps = node.ps;
|
|
448
|
+
for (const behavior of ps?.behaviors ?? []) {
|
|
449
|
+
if (
|
|
450
|
+
behavior.type === 'EmitSubParticleSystem' &&
|
|
451
|
+
'subParticleSystem' in behavior &&
|
|
452
|
+
typeof behavior.subParticleSystem === 'string'
|
|
453
|
+
) {
|
|
454
|
+
yield ['a sub emitter', behavior.subParticleSystem];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (ps?.shape?.type === 'mesh_surface' && typeof ps.shape.mesh === 'string') {
|
|
458
|
+
yield ['a mesh surface shape', ps.shape.mesh];
|
|
459
|
+
}
|
|
460
|
+
for (const animation of node.animationData ?? []) {
|
|
461
|
+
if (typeof animation.targetUUID === 'string') {
|
|
462
|
+
yield ['a prefab animation', animation.targetUUID];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** A texture's sampler state, each field only when it differs from a new Texture's. */
|
|
468
|
+
function samplerJSON(texture: Texture): Partial<QuarksTextureJSON> {
|
|
469
|
+
const json: Partial<QuarksTextureJSON> = {};
|
|
470
|
+
if (!texture.invertY) json.invertY = false;
|
|
471
|
+
if (texture.noMipmap) json.noMipmap = true;
|
|
472
|
+
if (texture.samplingMode !== Constants.TEXTURE_TRILINEAR_SAMPLINGMODE) json.samplingMode = texture.samplingMode;
|
|
473
|
+
if (texture.wrapU !== Texture.WRAP_ADDRESSMODE || texture.wrapV !== Texture.WRAP_ADDRESSMODE) {
|
|
474
|
+
json.wrap = [WRAP_TO_JSON[texture.wrapU] ?? 1000, WRAP_TO_JSON[texture.wrapV] ?? 1000];
|
|
475
|
+
}
|
|
476
|
+
if (texture.uScale !== 1 || texture.vScale !== 1) json.repeat = [texture.uScale, texture.vScale];
|
|
477
|
+
if (texture.uOffset !== 0 || texture.vOffset !== 0) json.offset = [texture.uOffset, texture.vOffset];
|
|
478
|
+
if (texture.wAng !== 0) json.rotation = texture.wAng;
|
|
479
|
+
return json;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Writes an effect, the node tree under `root` with its emitters, meshes and prefabs, as one JSON
|
|
484
|
+
* document `QuarksLoader` loads back: the counterpart of `loader.load()`. The result holds
|
|
485
|
+
* only plain data, so `JSON.stringify` takes it as is. Nodes keep their ids
|
|
486
|
+
* ({@link quarksNodeId}), so sub emitters, mesh surface shapes and prefab animations still find
|
|
487
|
+
* their targets, and a loaded effect saved again writes the same file.
|
|
488
|
+
*
|
|
489
|
+
* Textures are written by url, with their sampler state. One with no url a file can point at is
|
|
490
|
+
* written without its image and reported through `onWarning`. Several roots are written as
|
|
491
|
+
* children of one group.
|
|
492
|
+
*/
|
|
493
|
+
export function serializeEffect(
|
|
494
|
+
root: TransformNode | TransformNode[],
|
|
495
|
+
options: SerializeEffectOptions = {}
|
|
496
|
+
): QuarksEffectJSON {
|
|
497
|
+
const writer = new EffectWriter(options);
|
|
498
|
+
let object: QuarksNodeJSON;
|
|
499
|
+
if (Array.isArray(root)) {
|
|
500
|
+
const name = options.name ?? 'effect';
|
|
501
|
+
object = {
|
|
502
|
+
uuid: `${name}-root`,
|
|
503
|
+
type: 'Group',
|
|
504
|
+
name,
|
|
505
|
+
matrix: Matrix.Identity().asArray().slice(),
|
|
506
|
+
children: root.map((node) => writer.node(node)),
|
|
507
|
+
};
|
|
508
|
+
} else {
|
|
509
|
+
object = writer.node(root);
|
|
510
|
+
}
|
|
511
|
+
return writer.finish(object);
|
|
512
|
+
}
|