@ludra/three 0.95.0 → 0.96.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/dist/index.d.ts CHANGED
@@ -54,8 +54,9 @@ export declare class ThreeRenderer implements Renderer3D {
54
54
  /** Addons render hooks are still loading. */
55
55
  private readonly pending;
56
56
  private readonly def;
57
- private readonly assetBase;
58
- constructor(def: SceneDef, width: number, height: number, assetBase: string);
57
+ /** Loads go through this, so a .gltf's own relative files (its .bin, its textures) are found wherever assets live. */
58
+ private readonly manager;
59
+ constructor(def: SceneDef, width: number, height: number, assetUrl: (path: string) => string);
59
60
  /** Loads every texture and model the scene (and its prefabs) names, so the first frame (and screenshots) are complete. */
60
61
  load(): Promise<void>;
61
62
  /** Loads a glTF file once. Draco and meshopt compressed files work too. */
package/dist/index.js CHANGED
@@ -22,9 +22,15 @@ const TONE_MAPPING = {
22
22
  };
23
23
  /** Collider colors in the debug view, as in 2D: static, dynamic, kinematic, sensor, player. */
24
24
  const DEBUG_COLORS = { static: 0x7fdbff, dynamic: 0xffdc00, kinematic: 0xff851b, sensor: 0x2ecc40, player: 0xff4136 };
25
+ /**
26
+ * Loaders see project paths under this made-up scheme, which the loading manager turns into real
27
+ * URLs. three resolves a .gltf's relative files against it like any other base.
28
+ */
29
+ const PROJECT = "ludra:/";
25
30
  /** Creates the 3D renderer for a scene. Pass it to startGame as `loadRenderer3D`. */
26
31
  export const createRenderer3D = async (scene, options) => {
27
- const renderer = new ThreeRenderer(scene, options.width, options.height, options.assetBase);
32
+ const base = options.assetBase;
33
+ const renderer = new ThreeRenderer(scene, options.width, options.height, options.assetUrl ?? ((path) => base + path));
28
34
  await renderer.load();
29
35
  return renderer;
30
36
  };
@@ -67,10 +73,12 @@ export class ThreeRenderer {
67
73
  /** Addons render hooks are still loading. */
68
74
  pending = new Set();
69
75
  def;
70
- assetBase;
71
- constructor(def, width, height, assetBase) {
76
+ /** Loads go through this, so a .gltf's own relative files (its .bin, its textures) are found wherever assets live. */
77
+ manager;
78
+ constructor(def, width, height, assetUrl) {
72
79
  this.def = def;
73
- this.assetBase = assetBase;
80
+ this.manager = new THREE.LoadingManager();
81
+ this.manager.setURLModifier((url) => (url.startsWith(PROJECT) ? assetUrl(url.slice(PROJECT.length)) : url));
74
82
  this.canvas = document.createElement("canvas");
75
83
  this.canvas.width = width;
76
84
  this.canvas.height = height;
@@ -90,7 +98,7 @@ export class ThreeRenderer {
90
98
  }
91
99
  /** Loads every texture and model the scene (and its prefabs) names, so the first frame (and screenshots) are complete. */
92
100
  async load() {
93
- const loader = new THREE.TextureLoader();
101
+ const loader = new THREE.TextureLoader(this.manager);
94
102
  const meshes = [...this.def.entities, ...Object.values(this.def.prefabs ?? {})].map((e) => e.components?.mesh);
95
103
  const named = (field) => new Set(meshes.map((m) => m?.[field]).filter((p) => typeof p === "string" && p !== ""));
96
104
  const paths = named("texture");
@@ -99,7 +107,7 @@ export class ThreeRenderer {
99
107
  ...models,
100
108
  ...[...paths].map(async (path) => {
101
109
  try {
102
- const texture = await loader.loadAsync(this.assetBase + path);
110
+ const texture = await loader.loadAsync(PROJECT + path);
103
111
  texture.colorSpace = THREE.SRGBColorSpace;
104
112
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
105
113
  texture.anisotropy = 4;
@@ -117,14 +125,19 @@ export class ThreeRenderer {
117
125
  if (load)
118
126
  return load;
119
127
  if (!this.gltfLoader) {
120
- this.gltfLoader = new GLTFLoader();
128
+ this.gltfLoader = new GLTFLoader(this.manager);
121
129
  const draco = new DRACOLoader();
122
130
  // The decoder sits in three's addons, which the import map serves.
123
- draco.setDecoderPath(new URL("../libs/draco/gltf/", import.meta.resolve("three/addons/loaders/DRACOLoader.js")).href);
131
+ try {
132
+ draco.setDecoderPath(new URL("../libs/draco/gltf/", import.meta.resolve("three/addons/loaders/DRACOLoader.js")).href);
133
+ }
134
+ catch {
135
+ // A single-file build has no folder to load the decoder from: Draco models don't load there.
136
+ }
124
137
  this.gltfLoader.setDRACOLoader(draco);
125
138
  this.gltfLoader.setMeshoptDecoder(MeshoptDecoder);
126
139
  }
127
- load = this.track(this.gltfLoader.loadAsync(this.assetBase + path).then((gltf) => void this.gltfs.set(path, gltf), (err) => {
140
+ load = this.track(this.gltfLoader.loadAsync(PROJECT + path).then((gltf) => void this.gltfs.set(path, gltf), (err) => {
128
141
  console.warn(`[ludra] could not load model "${path}" (${err?.message ?? err}), drawing its shape instead.`);
129
142
  this.gltfs.set(path, null);
130
143
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ludra/three",
3
- "version": "0.95.0",
3
+ "version": "0.96.0",
4
4
  "description": "The 3D renderer for Ludra: Three.js meshes, PBR materials, lights, shadows, cameras and environments for 3D scenes.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "src"
32
32
  ],
33
33
  "dependencies": {
34
- "@ludra/core": "0.95.0",
34
+ "@ludra/core": "0.96.0",
35
35
  "three": "0.186.1"
36
36
  },
37
37
  "devDependencies": {
package/src/index.ts CHANGED
@@ -44,9 +44,16 @@ const TONE_MAPPING = {
44
44
  /** Collider colors in the debug view, as in 2D: static, dynamic, kinematic, sensor, player. */
45
45
  const DEBUG_COLORS = { static: 0x7fdbff, dynamic: 0xffdc00, kinematic: 0xff851b, sensor: 0x2ecc40, player: 0xff4136 } as const;
46
46
 
47
+ /**
48
+ * Loaders see project paths under this made-up scheme, which the loading manager turns into real
49
+ * URLs. three resolves a .gltf's relative files against it like any other base.
50
+ */
51
+ const PROJECT = "ludra:/";
52
+
47
53
  /** Creates the 3D renderer for a scene. Pass it to startGame as `loadRenderer3D`. */
48
54
  export const createRenderer3D: Renderer3DFactory = async (scene, options) => {
49
- const renderer = new ThreeRenderer(scene, options.width, options.height, options.assetBase);
55
+ const base = options.assetBase;
56
+ const renderer = new ThreeRenderer(scene, options.width, options.height, options.assetUrl ?? ((path) => base + path));
50
57
  await renderer.load();
51
58
  return renderer;
52
59
  };
@@ -115,11 +122,13 @@ export class ThreeRenderer implements Renderer3D {
115
122
  private readonly pending = new Set<Promise<unknown>>();
116
123
 
117
124
  private readonly def: SceneDef;
118
- private readonly assetBase: string;
125
+ /** Loads go through this, so a .gltf's own relative files (its .bin, its textures) are found wherever assets live. */
126
+ private readonly manager: THREE.LoadingManager;
119
127
 
120
- constructor(def: SceneDef, width: number, height: number, assetBase: string) {
128
+ constructor(def: SceneDef, width: number, height: number, assetUrl: (path: string) => string) {
121
129
  this.def = def;
122
- this.assetBase = assetBase;
130
+ this.manager = new THREE.LoadingManager();
131
+ this.manager.setURLModifier((url) => (url.startsWith(PROJECT) ? assetUrl(url.slice(PROJECT.length)) : url));
123
132
  this.canvas = document.createElement("canvas");
124
133
  this.canvas.width = width;
125
134
  this.canvas.height = height;
@@ -140,7 +149,7 @@ export class ThreeRenderer implements Renderer3D {
140
149
 
141
150
  /** Loads every texture and model the scene (and its prefabs) names, so the first frame (and screenshots) are complete. */
142
151
  async load(): Promise<void> {
143
- const loader = new THREE.TextureLoader();
152
+ const loader = new THREE.TextureLoader(this.manager);
144
153
  const meshes = [...this.def.entities, ...Object.values(this.def.prefabs ?? {})].map((e) => e.components?.mesh as Record<string, unknown> | undefined);
145
154
  const named = (field: string) => new Set(meshes.map((m) => m?.[field]).filter((p): p is string => typeof p === "string" && p !== ""));
146
155
  const paths = named("texture");
@@ -149,7 +158,7 @@ export class ThreeRenderer implements Renderer3D {
149
158
  ...models,
150
159
  ...[...paths].map(async (path) => {
151
160
  try {
152
- const texture = await loader.loadAsync(this.assetBase + path);
161
+ const texture = await loader.loadAsync(PROJECT + path);
153
162
  texture.colorSpace = THREE.SRGBColorSpace;
154
163
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
155
164
  texture.anisotropy = 4;
@@ -166,15 +175,19 @@ export class ThreeRenderer implements Renderer3D {
166
175
  let load = this.gltfLoads.get(path);
167
176
  if (load) return load;
168
177
  if (!this.gltfLoader) {
169
- this.gltfLoader = new GLTFLoader();
178
+ this.gltfLoader = new GLTFLoader(this.manager);
170
179
  const draco = new DRACOLoader();
171
180
  // The decoder sits in three's addons, which the import map serves.
172
- draco.setDecoderPath(new URL("../libs/draco/gltf/", import.meta.resolve("three/addons/loaders/DRACOLoader.js")).href);
181
+ try {
182
+ draco.setDecoderPath(new URL("../libs/draco/gltf/", import.meta.resolve("three/addons/loaders/DRACOLoader.js")).href);
183
+ } catch {
184
+ // A single-file build has no folder to load the decoder from: Draco models don't load there.
185
+ }
173
186
  this.gltfLoader.setDRACOLoader(draco);
174
187
  this.gltfLoader.setMeshoptDecoder(MeshoptDecoder);
175
188
  }
176
189
  load = this.track(
177
- this.gltfLoader.loadAsync(this.assetBase + path).then(
190
+ this.gltfLoader.loadAsync(PROJECT + path).then(
178
191
  (gltf) => void this.gltfs.set(path, gltf),
179
192
  (err: unknown) => {
180
193
  console.warn(`[ludra] could not load model "${path}" (${(err as Error)?.message ?? err}), drawing its shape instead.`);