Skip to content

Commit ec22fba

Browse files
committed
add lighting doc page with clone link to demo
1 parent 3e8e915 commit ec22fba

3 files changed

Lines changed: 150 additions & 1 deletion

File tree

docs/guide/lighting.mdx

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
---
2+
---
3+
4+
# Lighting
5+
6+
![Lighting effects in action](../../static/img/lighting-demo.png)
7+
8+
This page shows how to create dynamic lighting effects in Dreamlab using raycasting and PIXI graphics.
9+
10+
There is no built-in lighting system in Dreamlab. Instead, you can create lighting effects by combining raycasting for line-of-sight calculations with PIXI graphics for rendering shadows and illuminated areas.
11+
12+
## Light Overlay Example
13+
14+
Here's an example implementation of a lighting system using the `LightOverlay` behavior, which creates a dynamic shadow overlay that responds to the cursor position and world geometry.
15+
16+
```ts
17+
import { Behavior, BehaviorDestroyed, Collider, RawPixi, value } from "@dreamlab/engine";
18+
import * as PIXI from "@dreamlab/vendor/pixi.ts";
19+
import { Ray } from "@dreamlab/vendor/rapier.ts";
20+
21+
export default class LightOverlay extends Behavior {
22+
@value()
23+
rays: number = 7200;
24+
25+
@value()
26+
maxDistance: number = 100;
27+
28+
#pixi = this.entity.cast(RawPixi);
29+
#overlay!: PIXI.Graphics;
30+
#mask!: PIXI.Graphics;
31+
32+
#overlayCtx = new PIXI.GraphicsContext()
33+
.rect(-100, -100, 200, 200)
34+
.fill({ color: "black", alpha: 0.8 });
35+
36+
onInitialize(): void {
37+
if (!this.game.isClient()) return;
38+
if (!this.#pixi.container) return;
39+
40+
this.#mask = new PIXI.Graphics();
41+
this.#overlay = new PIXI.Graphics(this.#overlayCtx);
42+
this.#overlay.setMask({
43+
mask: this.#mask,
44+
inverse: true,
45+
});
46+
47+
this.#pixi.container.addChild(this.#mask);
48+
this.#pixi.container.addChild(this.#overlay);
49+
50+
this.on(BehaviorDestroyed, () => {
51+
this.#overlay?.destroy();
52+
this.#overlayCtx?.destroy();
53+
this.#mask?.destroy();
54+
});
55+
}
56+
57+
onFrame(): void {
58+
if (!this.game.isClient()) return;
59+
this.#mask.clear();
60+
61+
const { world } = this.game.inputs.cursor;
62+
if (!world) return;
63+
64+
const colliders = this.game.entities
65+
.lookupByPosition(world)
66+
.filter(entity => entity instanceof Collider);
67+
68+
if (colliders.length > 0) return;
69+
70+
const rays = this.rays;
71+
const toi = this.maxDistance;
72+
const solid = true;
73+
74+
this.#mask.moveTo(world.x, -world.y);
75+
for (let i = 0; i < rays + 1; i++) {
76+
const angle = (i / rays) * Math.PI * 2;
77+
const ray = new Ray(world, { x: Math.sin(angle), y: Math.cos(angle) });
78+
const hit = this.game.physics.world.castRay(ray, toi, solid);
79+
80+
const impact = hit?.timeOfImpact ?? toi;
81+
const point = ray.pointAt(impact - 0.01);
82+
83+
this.#mask.lineTo(point.x, -point.y);
84+
}
85+
86+
this.#mask.lineTo(world.x, -world.y).fill("white");
87+
}
88+
}
89+
```
90+
91+
:::tip Try it yourself!
92+
93+
Want to test this lighting effect? Clone this example project and open it in the Dreamlab editor. Click here:
94+
[Lighting Demo Project](https://app.dreamlab.gg/project/fork/u_kxz3lbxkp19woxvxzo3niwbz/lighting-demo)
95+
96+
:::
97+
98+
## How This Example Works
99+
100+
### Ray Casting for Visibility
101+
102+
This example casts rays in all directions from the cursor position:
103+
104+
1. **Ray Generation**: Creates rays in a 360-degree circle around the cursor
105+
2. **Collision Detection**: Each ray is cast until it hits a solid collider or reaches the maximum distance
106+
3. **Shadow Calculation**: The intersection points define the boundaries of the lit area
107+
108+
### Masking System
109+
110+
This implementation uses PIXI's masking system:
111+
112+
- **Overlay**: A dark semi-transparent rectangle that covers the entire screen
113+
- **Mask**: A white polygon shape that defines the lit area based on raycasting
114+
- **Inverse Masking**: The overlay is rendered everywhere except where the mask is present
115+
116+
### Example Properties
117+
118+
- `rays`: Number of rays to cast (higher values = smoother lighting, lower performance)
119+
- `maxDistance`: Maximum distance rays can travel before being stopped
120+
- `overlayCtx`: Graphics context defining the shadow overlay appearance
121+
122+
## Building Your Own Lighting
123+
124+
### Basic Concepts
125+
126+
- **Raycasting**: Use `Ray` from Rapier physics engine for line-of-sight calculations
127+
- **PIXI Graphics**: Create visual effects with masks, fills, and shapes
128+
- **Performance**: Balance ray count and update frequency for smooth gameplay
129+
130+
### Common Patterns
131+
132+
```ts
133+
// Cast a single ray to check visibility
134+
const ray = new Ray(lightPosition, direction);
135+
const hit = this.game.physics.world.castRay(ray, maxDistance, solid);
136+
137+
// Create a mask for lighting effects
138+
const mask = new PIXI.Graphics();
139+
mask.circle(x, y, radius).fill("white");
140+
someSprite.mask = mask;
141+
```
142+
143+
## Integration with Physics
144+
145+
Lighting effects can integrate with Dreamlab's physics system:
146+
147+
- Uses `Ray` from the Rapier physics engine for collision detection
148+
- Respects all solid colliders in the scene
149+
- Can be combined with dynamic obstacles for moving shadows

docs/guide/tilemaps.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export default class Checkerboard extends Behavior {
135135
}
136136

137137
onInitialize(): void {
138-
if (this.game.isServer()) this.#fill();
138+
if (this.game.isClient()) this.#fill(); // change to isServer() if you have this behavior in world/server root
139139
}
140140
}
141141
```

static/img/lighting-demo.png

32.4 KB
Loading

0 commit comments

Comments
 (0)