@ludra/three 0.92.0 → 0.94.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 +22 -1
- package/dist/index.js +93 -1
- package/package.json +2 -2
- package/src/index.ts +84 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { type Entity, type GameEvent, type RenderContext, type SceneDef } from "@ludra/core";
|
|
3
|
-
import type { Renderer3D, Renderer3DFactory } from "@ludra/core/browser";
|
|
3
|
+
import type { CameraView, Renderer3D, Renderer3DFactory } from "@ludra/core/browser";
|
|
4
4
|
/**
|
|
5
5
|
* The 3D renderer for Ludra: Three.js draws every entity with a `mesh` and lights the scene
|
|
6
6
|
* with its `light` entities, seen through a `camera3d`. It only reads the world, so it can
|
|
@@ -18,6 +18,12 @@ export declare const createRenderer3D: Renderer3DFactory;
|
|
|
18
18
|
export declare class ThreeRenderer implements Renderer3D {
|
|
19
19
|
readonly canvas: HTMLCanvasElement;
|
|
20
20
|
debug: boolean;
|
|
21
|
+
view: CameraView | undefined;
|
|
22
|
+
hidden: ReadonlySet<string> | undefined;
|
|
23
|
+
/** The map view's camera, fitted to the level every frame. */
|
|
24
|
+
private readonly mapCamera;
|
|
25
|
+
/** Lights the map evenly, so a night scene reads as well as a day one. */
|
|
26
|
+
private readonly mapLight;
|
|
21
27
|
/** Three.js itself, for render hooks and anything else (step 4 hands these to scripts). */
|
|
22
28
|
readonly renderer: THREE.WebGLRenderer;
|
|
23
29
|
readonly scene: THREE.Scene<THREE.Object3DEventMap>;
|
|
@@ -58,6 +64,21 @@ export declare class ThreeRenderer implements Renderer3D {
|
|
|
58
64
|
step(world: WorldView): void;
|
|
59
65
|
reset(world: WorldView): void;
|
|
60
66
|
render(world: WorldView): void;
|
|
67
|
+
/** Hides what the hidden entities show. Their lights stay on: what's behind them must look the same. */
|
|
68
|
+
private conceal;
|
|
69
|
+
/** In the map view: every entity with a body or mesh, at its place on the map. Ground-sized ones are left out. */
|
|
70
|
+
labels(world: WorldView): {
|
|
71
|
+
text: string;
|
|
72
|
+
x: number;
|
|
73
|
+
y: number;
|
|
74
|
+
}[];
|
|
75
|
+
/** Free view: from a fixed point, looking at a point or following an entity. */
|
|
76
|
+
private placeFreeCamera;
|
|
77
|
+
/**
|
|
78
|
+
* Map view: straight down on everything drawn, north (-z) up, with no perspective so
|
|
79
|
+
* distances read true. Fog is off and the light is even: the map is about where things are.
|
|
80
|
+
*/
|
|
81
|
+
private renderMap;
|
|
61
82
|
/** Waits for every addon render hooks asked for, and for what their callbacks start in turn. */
|
|
62
83
|
settle(): Promise<void>;
|
|
63
84
|
private track;
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,12 @@ export const createRenderer3D = async (scene, options) => {
|
|
|
31
31
|
export class ThreeRenderer {
|
|
32
32
|
canvas;
|
|
33
33
|
debug = false;
|
|
34
|
+
view;
|
|
35
|
+
hidden;
|
|
36
|
+
/** The map view's camera, fitted to the level every frame. */
|
|
37
|
+
mapCamera = new THREE.OrthographicCamera();
|
|
38
|
+
/** Lights the map evenly, so a night scene reads as well as a day one. */
|
|
39
|
+
mapLight = new THREE.AmbientLight(0xffffff, 3);
|
|
34
40
|
/** Three.js itself, for render hooks and anything else (step 4 hands these to scripts). */
|
|
35
41
|
renderer;
|
|
36
42
|
scene = new THREE.Scene();
|
|
@@ -172,12 +178,98 @@ export class ThreeRenderer {
|
|
|
172
178
|
const rig = this.rig;
|
|
173
179
|
this.camera.position.copy(rig.position);
|
|
174
180
|
this.camera.lookAt(rig.look);
|
|
181
|
+
if (this.view?.mode === "free")
|
|
182
|
+
this.placeFreeCamera(world, this.view);
|
|
175
183
|
this.poseModels(world);
|
|
176
184
|
this.runRenderHooks(world);
|
|
177
|
-
|
|
185
|
+
const concealed = this.conceal();
|
|
186
|
+
if (this.view?.mode === "map")
|
|
187
|
+
this.renderMap();
|
|
188
|
+
else if (this.customRender)
|
|
178
189
|
this.customRender();
|
|
179
190
|
else
|
|
180
191
|
this.renderer.render(this.scene, this.camera);
|
|
192
|
+
for (const object of concealed)
|
|
193
|
+
object.visible = true;
|
|
194
|
+
}
|
|
195
|
+
/** Hides what the hidden entities show. Their lights stay on: what's behind them must look the same. */
|
|
196
|
+
conceal() {
|
|
197
|
+
const concealed = [];
|
|
198
|
+
if (!this.hidden?.size)
|
|
199
|
+
return concealed;
|
|
200
|
+
const roots = [...this.objects, ...this.hookGroups].filter(([e]) => this.hidden.has(e.id));
|
|
201
|
+
for (const [, root] of roots) {
|
|
202
|
+
root.traverse((object) => {
|
|
203
|
+
const shape = object;
|
|
204
|
+
if (!object.visible || !(shape.isMesh || shape.isLine || shape.isPoints || shape.isSprite))
|
|
205
|
+
return;
|
|
206
|
+
object.visible = false;
|
|
207
|
+
concealed.push(object);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return concealed;
|
|
211
|
+
}
|
|
212
|
+
/** In the map view: every entity with a body or mesh, at its place on the map. Ground-sized ones are left out. */
|
|
213
|
+
labels(world) {
|
|
214
|
+
if (this.view?.mode !== "map")
|
|
215
|
+
return [];
|
|
216
|
+
const { width, height } = this.canvas;
|
|
217
|
+
const cam = this.mapCamera;
|
|
218
|
+
const mapArea = (cam.right - cam.left) * (cam.top - cam.bottom);
|
|
219
|
+
const labels = [];
|
|
220
|
+
const size = new THREE.Vector3();
|
|
221
|
+
for (const e of world.entities) {
|
|
222
|
+
if (!e.body && !e.mesh)
|
|
223
|
+
continue;
|
|
224
|
+
const object = this.objects.get(e);
|
|
225
|
+
if (object && new THREE.Box3().setFromObject(object).getSize(size) && size.x * size.z > mapArea * 0.2)
|
|
226
|
+
continue;
|
|
227
|
+
const point = new THREE.Vector3(e.transform.x, e.transform.y, e.transform.z).project(cam);
|
|
228
|
+
if (Math.abs(point.x) > 1 || Math.abs(point.y) > 1)
|
|
229
|
+
continue;
|
|
230
|
+
labels.push({ text: e.id, x: ((point.x + 1) / 2) * width, y: ((1 - point.y) / 2) * height });
|
|
231
|
+
}
|
|
232
|
+
return labels;
|
|
233
|
+
}
|
|
234
|
+
/** Free view: from a fixed point, looking at a point or following an entity. */
|
|
235
|
+
placeFreeCamera(world, view) {
|
|
236
|
+
this.camera.position.set(view.from.x, view.from.y, view.from.z);
|
|
237
|
+
if (typeof view.at === "string") {
|
|
238
|
+
const target = world.entities.find((e) => e.id === view.at);
|
|
239
|
+
const t = target?.transform ?? { x: 0, y: 0, z: 0 };
|
|
240
|
+
this.camera.lookAt(t.x, t.y, t.z);
|
|
241
|
+
}
|
|
242
|
+
else
|
|
243
|
+
this.camera.lookAt(view.at.x, view.at.y, view.at.z);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Map view: straight down on everything drawn, north (-z) up, with no perspective so
|
|
247
|
+
* distances read true. Fog is off and the light is even: the map is about where things are.
|
|
248
|
+
*/
|
|
249
|
+
renderMap() {
|
|
250
|
+
const bounds = new THREE.Box3();
|
|
251
|
+
for (const object of this.objects.values())
|
|
252
|
+
if (object.visible)
|
|
253
|
+
bounds.expandByObject(object);
|
|
254
|
+
if (bounds.isEmpty())
|
|
255
|
+
bounds.set(new THREE.Vector3(-10, 0, -10), new THREE.Vector3(10, 0, 10));
|
|
256
|
+
const center = bounds.getCenter(new THREE.Vector3());
|
|
257
|
+
const size = bounds.getSize(new THREE.Vector3());
|
|
258
|
+
const aspect = this.canvas.width / this.canvas.height;
|
|
259
|
+
const halfWidth = Math.max(size.x / 2, (size.z / 2) * aspect, 1) * 1.08;
|
|
260
|
+
const cam = this.mapCamera;
|
|
261
|
+
Object.assign(cam, { left: -halfWidth, right: halfWidth, top: halfWidth / aspect, bottom: -halfWidth / aspect, near: 0.1, far: size.y + 40 });
|
|
262
|
+
cam.position.set(center.x, bounds.max.y + 20, center.z);
|
|
263
|
+
cam.up.set(0, 0, -1);
|
|
264
|
+
cam.lookAt(center.x, bounds.min.y, center.z);
|
|
265
|
+
cam.updateProjectionMatrix();
|
|
266
|
+
cam.updateMatrixWorld();
|
|
267
|
+
const fog = this.scene.fog;
|
|
268
|
+
this.scene.fog = null;
|
|
269
|
+
this.scene.add(this.mapLight);
|
|
270
|
+
this.renderer.render(this.scene, cam);
|
|
271
|
+
this.scene.remove(this.mapLight);
|
|
272
|
+
this.scene.fog = fog;
|
|
181
273
|
}
|
|
182
274
|
/** Waits for every addon render hooks asked for, and for what their callbacks start in turn. */
|
|
183
275
|
async settle() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ludra/three",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.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.
|
|
34
|
+
"@ludra/core": "0.94.0",
|
|
35
35
|
"three": "0.186.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type RenderContext,
|
|
19
19
|
type SceneDef,
|
|
20
20
|
} from "@ludra/core";
|
|
21
|
-
import type { Renderer3D, Renderer3DFactory } from "@ludra/core/browser";
|
|
21
|
+
import type { CameraView, Renderer3D, Renderer3DFactory } from "@ludra/core/browser";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* The 3D renderer for Ludra: Three.js draws every entity with a `mesh` and lights the scene
|
|
@@ -78,6 +78,12 @@ interface Rig {
|
|
|
78
78
|
export class ThreeRenderer implements Renderer3D {
|
|
79
79
|
readonly canvas: HTMLCanvasElement;
|
|
80
80
|
debug = false;
|
|
81
|
+
view: CameraView | undefined;
|
|
82
|
+
hidden: ReadonlySet<string> | undefined;
|
|
83
|
+
/** The map view's camera, fitted to the level every frame. */
|
|
84
|
+
private readonly mapCamera = new THREE.OrthographicCamera();
|
|
85
|
+
/** Lights the map evenly, so a night scene reads as well as a day one. */
|
|
86
|
+
private readonly mapLight = new THREE.AmbientLight(0xffffff, 3);
|
|
81
87
|
/** Three.js itself, for render hooks and anything else (step 4 hands these to scripts). */
|
|
82
88
|
readonly renderer: THREE.WebGLRenderer;
|
|
83
89
|
readonly scene = new THREE.Scene();
|
|
@@ -227,10 +233,86 @@ export class ThreeRenderer implements Renderer3D {
|
|
|
227
233
|
const rig = this.rig!;
|
|
228
234
|
this.camera.position.copy(rig.position);
|
|
229
235
|
this.camera.lookAt(rig.look);
|
|
236
|
+
if (this.view?.mode === "free") this.placeFreeCamera(world, this.view);
|
|
230
237
|
this.poseModels(world);
|
|
231
238
|
this.runRenderHooks(world);
|
|
232
|
-
|
|
239
|
+
const concealed = this.conceal();
|
|
240
|
+
if (this.view?.mode === "map") this.renderMap();
|
|
241
|
+
else if (this.customRender) this.customRender();
|
|
233
242
|
else this.renderer.render(this.scene, this.camera);
|
|
243
|
+
for (const object of concealed) object.visible = true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Hides what the hidden entities show. Their lights stay on: what's behind them must look the same. */
|
|
247
|
+
private conceal(): THREE.Object3D[] {
|
|
248
|
+
const concealed: THREE.Object3D[] = [];
|
|
249
|
+
if (!this.hidden?.size) return concealed;
|
|
250
|
+
const roots = [...this.objects, ...this.hookGroups].filter(([e]) => this.hidden!.has(e.id));
|
|
251
|
+
for (const [, root] of roots) {
|
|
252
|
+
root.traverse((object) => {
|
|
253
|
+
const shape = object as Partial<THREE.Mesh & THREE.Line & THREE.Points & THREE.Sprite>;
|
|
254
|
+
if (!object.visible || !(shape.isMesh || shape.isLine || shape.isPoints || shape.isSprite)) return;
|
|
255
|
+
object.visible = false;
|
|
256
|
+
concealed.push(object);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return concealed;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** In the map view: every entity with a body or mesh, at its place on the map. Ground-sized ones are left out. */
|
|
263
|
+
labels(world: WorldView): { text: string; x: number; y: number }[] {
|
|
264
|
+
if (this.view?.mode !== "map") return [];
|
|
265
|
+
const { width, height } = this.canvas;
|
|
266
|
+
const cam = this.mapCamera;
|
|
267
|
+
const mapArea = (cam.right - cam.left) * (cam.top - cam.bottom);
|
|
268
|
+
const labels: { text: string; x: number; y: number }[] = [];
|
|
269
|
+
const size = new THREE.Vector3();
|
|
270
|
+
for (const e of world.entities) {
|
|
271
|
+
if (!e.body && !e.mesh) continue;
|
|
272
|
+
const object = this.objects.get(e);
|
|
273
|
+
if (object && new THREE.Box3().setFromObject(object).getSize(size) && size.x * size.z > mapArea * 0.2) continue;
|
|
274
|
+
const point = new THREE.Vector3(e.transform.x, e.transform.y, e.transform.z).project(cam);
|
|
275
|
+
if (Math.abs(point.x) > 1 || Math.abs(point.y) > 1) continue;
|
|
276
|
+
labels.push({ text: e.id, x: ((point.x + 1) / 2) * width, y: ((1 - point.y) / 2) * height });
|
|
277
|
+
}
|
|
278
|
+
return labels;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Free view: from a fixed point, looking at a point or following an entity. */
|
|
282
|
+
private placeFreeCamera(world: WorldView, view: Extract<CameraView, { mode: "free" }>): void {
|
|
283
|
+
this.camera.position.set(view.from.x, view.from.y, view.from.z);
|
|
284
|
+
if (typeof view.at === "string") {
|
|
285
|
+
const target = world.entities.find((e) => e.id === view.at);
|
|
286
|
+
const t = target?.transform ?? { x: 0, y: 0, z: 0 };
|
|
287
|
+
this.camera.lookAt(t.x, t.y, t.z);
|
|
288
|
+
} else this.camera.lookAt(view.at.x, view.at.y, view.at.z);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Map view: straight down on everything drawn, north (-z) up, with no perspective so
|
|
293
|
+
* distances read true. Fog is off and the light is even: the map is about where things are.
|
|
294
|
+
*/
|
|
295
|
+
private renderMap(): void {
|
|
296
|
+
const bounds = new THREE.Box3();
|
|
297
|
+
for (const object of this.objects.values()) if (object.visible) bounds.expandByObject(object);
|
|
298
|
+
if (bounds.isEmpty()) bounds.set(new THREE.Vector3(-10, 0, -10), new THREE.Vector3(10, 0, 10));
|
|
299
|
+
const center = bounds.getCenter(new THREE.Vector3());
|
|
300
|
+
const size = bounds.getSize(new THREE.Vector3());
|
|
301
|
+
const aspect = this.canvas.width / this.canvas.height;
|
|
302
|
+
const halfWidth = Math.max(size.x / 2, (size.z / 2) * aspect, 1) * 1.08;
|
|
303
|
+
const cam = this.mapCamera;
|
|
304
|
+
Object.assign(cam, { left: -halfWidth, right: halfWidth, top: halfWidth / aspect, bottom: -halfWidth / aspect, near: 0.1, far: size.y + 40 });
|
|
305
|
+
cam.position.set(center.x, bounds.max.y + 20, center.z);
|
|
306
|
+
cam.up.set(0, 0, -1);
|
|
307
|
+
cam.lookAt(center.x, bounds.min.y, center.z);
|
|
308
|
+
cam.updateProjectionMatrix();
|
|
309
|
+
cam.updateMatrixWorld();
|
|
310
|
+
const fog = this.scene.fog;
|
|
311
|
+
this.scene.fog = null;
|
|
312
|
+
this.scene.add(this.mapLight);
|
|
313
|
+
this.renderer.render(this.scene, cam);
|
|
314
|
+
this.scene.remove(this.mapLight);
|
|
315
|
+
this.scene.fog = fog;
|
|
234
316
|
}
|
|
235
317
|
|
|
236
318
|
/** Waits for every addon render hooks asked for, and for what their callbacks start in turn. */
|