@dcl-regenesislabs/opendcl 0.2.1-26165320302.commit-e6effe4 → 0.2.1-26238928766.commit-28648d7
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/README.md +5 -3
- package/context/sdk7-cheat-sheet.md +4 -0
- package/dist/index.js +0 -12
- package/dist/index.js.map +1 -1
- package/extensions/dcl-init.ts +58 -6
- package/package.json +3 -3
- package/prompts/system.md +71 -41
- package/skills/add-3d-models/SKILL.md +120 -70
- package/skills/add-interactivity/SKILL.md +74 -2
- package/skills/advanced-input/SKILL.md +34 -1
- package/skills/advanced-rendering/SKILL.md +82 -9
- package/skills/animations-tweens/SKILL.md +203 -98
- package/skills/audio-analysis/SKILL.md +164 -0
- package/skills/audio-video/SKILL.md +184 -83
- package/skills/build-ui/SKILL.md +25 -2
- package/skills/camera-control/SKILL.md +78 -7
- package/skills/create-scene/SKILL.md +56 -13
- package/skills/deploy-scene/SKILL.md +12 -0
- package/skills/deploy-worlds/SKILL.md +35 -0
- package/skills/editor-gizmo/.gitignore +11 -0
- package/skills/editor-gizmo/SKILL.md +222 -0
- package/skills/editor-gizmo/src/__editor/camera.ts +277 -0
- package/skills/editor-gizmo/src/__editor/discovery.ts +186 -0
- package/skills/editor-gizmo/src/__editor/drag.ts +265 -0
- package/skills/editor-gizmo/src/__editor/gizmo.ts +496 -0
- package/skills/editor-gizmo/src/__editor/history.ts +72 -0
- package/skills/editor-gizmo/src/__editor/index.ts +137 -0
- package/skills/editor-gizmo/src/__editor/input.ts +55 -0
- package/skills/editor-gizmo/src/__editor/math-utils.ts +114 -0
- package/skills/editor-gizmo/src/__editor/persistence.ts +113 -0
- package/skills/editor-gizmo/src/__editor/selection.ts +157 -0
- package/skills/editor-gizmo/src/__editor/state.ts +117 -0
- package/skills/editor-gizmo/src/__editor/ui.tsx +697 -0
- package/skills/game-design/SKILL.md +1 -2
- package/skills/lighting-environment/SKILL.md +103 -56
- package/skills/multiplayer-sync/SKILL.md +31 -2
- package/skills/nft-blockchain/SKILL.md +45 -40
- package/skills/npcs/SKILL.md +180 -0
- package/skills/optimize-scene/SKILL.md +7 -2
- package/skills/particle-system/SKILL.md +222 -0
- package/skills/player-avatar/SKILL.md +133 -7
- package/skills/player-physics/SKILL.md +93 -0
- package/skills/scene-runtime/SKILL.md +9 -5
- package/skills/visual-feedback/SKILL.md +1 -0
- package/extensions/dcl-setup-ollama.ts +0 -312
|
@@ -5,27 +5,30 @@ description: Add 3D models (.glb/.gltf) to a Decentraland scene using GltfContai
|
|
|
5
5
|
|
|
6
6
|
# Adding 3D Models to Decentraland Scenes
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Where models go: `main-entities.ts`
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
A 3D model placed at author time is a static visible entity. **Declare it in `main-entities.ts`**, not via `engine.addEntity()` in `src/index.ts`. The build compiles `main-entities.ts` into `main.crdt`, the engine preloads it before `main()` runs, and the editor can drag/rotate the model interactively.
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
13
|
+
// main-entities.ts
|
|
14
|
+
import type { Scene } from '@dcl/sdk/scene-types'
|
|
15
|
+
|
|
16
|
+
export const scene = {
|
|
17
|
+
my_model: {
|
|
18
|
+
components: {
|
|
19
|
+
Transform: { position: { x: 8, y: 0, z: 8 } },
|
|
20
|
+
GltfContainer: {
|
|
21
|
+
src: 'models/myModel.glb',
|
|
22
|
+
visibleMeshesCollisionMask: 3 // CL_PHYSICS | CL_POINTER
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} satisfies Scene
|
|
26
27
|
```
|
|
27
28
|
|
|
28
|
-
> **Always set `visibleMeshesCollisionMask`**
|
|
29
|
+
> **Always set `visibleMeshesCollisionMask`** on `GltfContainer`. Catalog models don't include separate collider meshes — using the visible mesh as the collider ensures the model is solid and clickable. Use the integer value (`ColliderLayer.CL_PHYSICS = 1`, `CL_POINTER = 2`, both = `3`) inside `main-entities.ts` since enums aren't allowed in the literal.
|
|
30
|
+
|
|
31
|
+
**When to use `engine.addEntity()` in `src/index.ts` instead**: only when the model is spawned dynamically (procedurally placed in a loop, dropped on an event, gated by NFT ownership, etc.). For static props, always use `main-entities.ts`.
|
|
29
32
|
|
|
30
33
|
## File Organization
|
|
31
34
|
|
|
@@ -46,52 +49,80 @@ project/
|
|
|
46
49
|
## Colliders
|
|
47
50
|
|
|
48
51
|
### Using Model's Built-in Colliders
|
|
49
|
-
|
|
52
|
+
|
|
50
53
|
```typescript
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// main-entities.ts — declared inline with GltfContainer
|
|
55
|
+
building: {
|
|
56
|
+
components: {
|
|
57
|
+
Transform: { position: { x: 8, y: 0, z: 8 } },
|
|
58
|
+
GltfContainer: {
|
|
59
|
+
src: 'models/building.glb',
|
|
60
|
+
visibleMeshesCollisionMask: 3, // CL_PHYSICS | CL_POINTER
|
|
61
|
+
invisibleMeshesCollisionMask: 1 // CL_PHYSICS
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
56
65
|
```
|
|
57
66
|
|
|
58
67
|
### Adding Simple Colliders
|
|
59
|
-
|
|
68
|
+
|
|
69
|
+
For basic shapes (no GLTF), add `MeshCollider`:
|
|
70
|
+
|
|
60
71
|
```typescript
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
// main-entities.ts
|
|
73
|
+
invisible_wall: {
|
|
74
|
+
components: {
|
|
75
|
+
Transform: { position: { x: 0, y: 1, z: 8 }, scale: { x: 0.1, y: 2, z: 16 } },
|
|
76
|
+
MeshCollider: { mesh: { $case: 'box', box: { uvs: [] } } }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
64
79
|
```
|
|
65
80
|
|
|
81
|
+
Other shapes: `{ $case: 'sphere', sphere: {} }`, `{ $case: 'plane', plane: { uvs: [] } }`, `{ $case: 'cylinder', cylinder: {} }`.
|
|
82
|
+
|
|
83
|
+
## ⚠️ Important: Never Pass `undefined` in Transform Fields
|
|
84
|
+
|
|
85
|
+
The SDK serializer crashes if any Transform field (`position`, `rotation`, `scale`) is present but `undefined`. **Omit the key entirely** instead — both in `main-entities.ts` literals and in any runtime helpers. In `main-entities.ts` this is natural (you just don't write the field).
|
|
86
|
+
|
|
66
87
|
## Common Model Operations
|
|
67
88
|
|
|
68
89
|
### Scaling
|
|
69
|
-
```typescript
|
|
70
|
-
Transform.create(model, {
|
|
71
|
-
position: Vector3.create(8, 0, 8),
|
|
72
|
-
scale: Vector3.create(2, 2, 2) // 2x size
|
|
73
|
-
})
|
|
74
|
-
```
|
|
75
90
|
|
|
76
|
-
### Rotation
|
|
77
91
|
```typescript
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
92
|
+
// main-entities.ts
|
|
93
|
+
big_statue: {
|
|
94
|
+
components: {
|
|
95
|
+
Transform: { position: { x: 8, y: 0, z: 8 }, scale: { x: 2, y: 2, z: 2 } },
|
|
96
|
+
GltfContainer: { src: 'models/statue.glb' }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
82
99
|
```
|
|
83
100
|
|
|
84
|
-
###
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
### Rotation (Euler angles converted to a quaternion at author time)
|
|
102
|
+
|
|
103
|
+
Quaternions are `{ x, y, z, w }`. For `Quaternion.fromEulerDegrees(0, 90, 0)` the equivalent literal is `{ x: 0, y: 0.7071, z: 0, w: 0.7071 }`. If you need exact-degree rotations and don't want to compute by hand, set `rotation` to identity in `main-entities.ts` and rotate at runtime in `src/index.ts` using `Transform.getMutable(entity).rotation = Quaternion.fromEulerDegrees(0, 90, 0)`.
|
|
104
|
+
|
|
105
|
+
### Parenting
|
|
106
|
+
|
|
107
|
+
Reference the parent by **name** (a string key from the same `scene` object). The build resolves names to entity IDs in a second pass.
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
GltfContainer
|
|
109
|
+
```typescript
|
|
110
|
+
// main-entities.ts
|
|
111
|
+
character: {
|
|
112
|
+
components: {
|
|
113
|
+
Transform: { position: { x: 8, y: 0, z: 8 } },
|
|
114
|
+
GltfContainer: { src: 'models/character.glb' }
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
hat: {
|
|
118
|
+
components: {
|
|
119
|
+
Transform: {
|
|
120
|
+
position: { x: 0, y: 2, z: 0 }, // 2m above parent's origin
|
|
121
|
+
parent: 'character'
|
|
122
|
+
},
|
|
123
|
+
GltfContainer: { src: 'models/hat.glb' }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
95
126
|
```
|
|
96
127
|
|
|
97
128
|
## Free 3D Models — OpenDCL Catalog (5,700+ models)
|
|
@@ -146,19 +177,34 @@ curl -o models/zombie-purple.glb "https://models.dclregenesislabs.xyz/blobs/bafy
|
|
|
146
177
|
```
|
|
147
178
|
|
|
148
179
|
```typescript
|
|
149
|
-
//
|
|
150
|
-
import {
|
|
151
|
-
|
|
180
|
+
// main-entities.ts — declare the entity with all its initial state
|
|
181
|
+
import type { Scene } from '@dcl/sdk/scene-types'
|
|
182
|
+
|
|
183
|
+
export const scene = {
|
|
184
|
+
zombie: {
|
|
185
|
+
components: {
|
|
186
|
+
Transform: { position: { x: 8, y: 0, z: 8 } },
|
|
187
|
+
GltfContainer: { src: 'models/zombie-purple.glb' },
|
|
188
|
+
Animator: {
|
|
189
|
+
states: [
|
|
190
|
+
{ clip: 'ZombieWalk', playing: true, loop: true },
|
|
191
|
+
{ clip: 'ZombieAttack', playing: false, loop: false }
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} satisfies Scene
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
To switch animations at runtime (e.g., trigger attack on click), use `src/index.ts`:
|
|
152
200
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
]
|
|
161
|
-
})
|
|
201
|
+
```typescript
|
|
202
|
+
import { engine, Animator } from '@dcl/sdk/ecs'
|
|
203
|
+
|
|
204
|
+
export function main() {
|
|
205
|
+
const zombie = engine.getEntityOrNullByName('zombie')
|
|
206
|
+
if (zombie) Animator.playSingleAnimation(zombie, 'ZombieAttack')
|
|
207
|
+
}
|
|
162
208
|
```
|
|
163
209
|
|
|
164
210
|
> **Important**: `GltfContainer` only works with **local files**. Never use external URLs for the model `src` field. Always download models into `models/` first.
|
|
@@ -166,19 +212,23 @@ Animator.create(zombie, {
|
|
|
166
212
|
|
|
167
213
|
### Checking Model Load State
|
|
168
214
|
|
|
169
|
-
|
|
215
|
+
Load-state polling is runtime behavior — put it in `src/index.ts` and reference the entity by name:
|
|
170
216
|
|
|
171
217
|
```typescript
|
|
172
|
-
import {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
})
|
|
218
|
+
import { engine, GltfContainerLoadingState, LoadingState } from '@dcl/sdk/ecs'
|
|
219
|
+
|
|
220
|
+
export function main() {
|
|
221
|
+
engine.addSystem(() => {
|
|
222
|
+
const model = engine.getEntityOrNullByName('zombie')
|
|
223
|
+
if (!model) return
|
|
224
|
+
const state = GltfContainerLoadingState.getOrNull(model)
|
|
225
|
+
if (state?.currentState === LoadingState.FINISHED) {
|
|
226
|
+
console.log('Model loaded successfully')
|
|
227
|
+
} else if (state?.currentState === LoadingState.FINISHED_WITH_ERROR) {
|
|
228
|
+
console.log('Model failed to load')
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
182
232
|
```
|
|
183
233
|
|
|
184
234
|
## Troubleshooting
|
|
@@ -5,6 +5,37 @@ description: Add click handlers, hover effects, pointer events, trigger areas, r
|
|
|
5
5
|
|
|
6
6
|
# Adding Interactivity to Decentraland Scenes
|
|
7
7
|
|
|
8
|
+
## Authoring split
|
|
9
|
+
|
|
10
|
+
The clickable entity (cube, button, model) is static — declare it in `main-entities.ts` with its Transform / Mesh / Material. The clickability itself is **always** added at runtime in `src/index.ts` via `pointerEventsSystem.onPointerDown(...)` (or the related helpers). The helper writes the `PointerEvents` component AND registers the callback in a single call — do NOT also declare `PointerEvents` in `main-entities.ts`; the helper would just overwrite it and the duplication invites drift.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// main-entities.ts — entity only, no PointerEvents
|
|
14
|
+
clickable_cube: {
|
|
15
|
+
components: {
|
|
16
|
+
Transform: { position: { x: 8, y: 1, z: 8 } },
|
|
17
|
+
MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// src/index.ts — register clickability via the helper system
|
|
24
|
+
import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
|
|
25
|
+
|
|
26
|
+
export function main() {
|
|
27
|
+
const cube = engine.getEntityOrNullByName('clickable_cube')
|
|
28
|
+
if (cube) {
|
|
29
|
+
pointerEventsSystem.onPointerDown(
|
|
30
|
+
{ entity: cube, opts: { button: InputAction.IA_POINTER, hoverText: 'Open' } },
|
|
31
|
+
() => { /* what happens on click */ }
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`TriggerArea` and `Raycast` are also runtime — they live in `src/index.ts`. Code examples below that create entities inline with `engine.addEntity()` are for runtime/technical entities (raycast probes, trigger volumes generated from data); for static clickable props, declare the prop in `main-entities.ts` and attach handlers in `src/index.ts` as above.
|
|
38
|
+
|
|
8
39
|
## Decision Tree
|
|
9
40
|
|
|
10
41
|
| Need | Approach | API |
|
|
@@ -89,10 +120,10 @@ pointerEventsSystem.removeOnPointerUp(cube)
|
|
|
89
120
|
```
|
|
90
121
|
|
|
91
122
|
### Important: Colliders Required
|
|
92
|
-
Pointer events only work on entities with a **collider**. Add one if your entity doesn't have a mesh:
|
|
123
|
+
Pointer events only work on entities with a **collider on the `CL_POINTER` layer**. Add one if your entity doesn't have a mesh:
|
|
93
124
|
```typescript
|
|
94
125
|
import { MeshCollider } from '@dcl/sdk/ecs'
|
|
95
|
-
MeshCollider.setBox(entity) // Invisible box collider
|
|
126
|
+
MeshCollider.setBox(entity) // Invisible box collider — defaults include CL_POINTER
|
|
96
127
|
```
|
|
97
128
|
|
|
98
129
|
For GLTF models, set the collision mask:
|
|
@@ -105,6 +136,47 @@ GltfContainer.create(entity, {
|
|
|
105
136
|
|
|
106
137
|
---
|
|
107
138
|
|
|
139
|
+
## Proximity Events (Pointer-Free Triggers)
|
|
140
|
+
|
|
141
|
+
Like `pointerEventsSystem.onPointerDown`, but fires based on **player distance** to the entity instead of a click. Useful for "press E when near" interactions and signposts that highlight on approach. No collider required — the system polls the player position vs the entity transform.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
|
|
145
|
+
|
|
146
|
+
const door = engine.getEntityOrNullByName('shop_door')
|
|
147
|
+
if (door) {
|
|
148
|
+
pointerEventsSystem.onProximityDown(
|
|
149
|
+
{
|
|
150
|
+
entity: door,
|
|
151
|
+
opts: {
|
|
152
|
+
button: InputAction.IA_PRIMARY,
|
|
153
|
+
hoverText: 'Open shop',
|
|
154
|
+
maxPlayerDistance: 3 // metres
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
() => { /* run when the player presses the button within range */ }
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
pointerEventsSystem.onProximityEnter(
|
|
161
|
+
{ entity: door, opts: { maxPlayerDistance: 5 } },
|
|
162
|
+
() => { /* fired once when the player enters the radius */ }
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
pointerEventsSystem.onProximityLeave(
|
|
166
|
+
{ entity: door, opts: { maxPlayerDistance: 5 } },
|
|
167
|
+
() => { /* fired once when the player leaves the radius */ }
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- `maxPlayerDistance` is required and is measured from the **avatar root**, not the camera.
|
|
173
|
+
- `priority` (number) — if multiple proximity events overlap, the higher value wins.
|
|
174
|
+
- Remove with `pointerEventsSystem.removeOnProximityDown(entity)` etc.
|
|
175
|
+
|
|
176
|
+
Prefer **proximity events** over `pointerEventsSystem.onPointerDown` when the entity has no visible collider or when the player shouldn't need to aim at it (signs, doors that just open when approached, etc.).
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
108
180
|
## Trigger Areas (Proximity Detection)
|
|
109
181
|
|
|
110
182
|
Detect when the player enters, exits, or stays inside an area:
|
|
@@ -83,6 +83,8 @@ engine.addSystem(myInputSystem)
|
|
|
83
83
|
|
|
84
84
|
### Global Input Checks
|
|
85
85
|
|
|
86
|
+
Fires regardless of whether the player's cursor was over an entity.
|
|
87
|
+
|
|
86
88
|
```typescript
|
|
87
89
|
function globalInputSystem() {
|
|
88
90
|
// Was the key just pressed this frame?
|
|
@@ -99,6 +101,31 @@ function globalInputSystem() {
|
|
|
99
101
|
engine.addSystem(globalInputSystem)
|
|
100
102
|
```
|
|
101
103
|
|
|
104
|
+
### Tag-Based Input Batching
|
|
105
|
+
|
|
106
|
+
If you have many similar entities that all respond to the same input (e.g., every barrel responds to E to break), tag them via the `Tag` component and iterate the tag query each frame:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { engine, Tag, inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
|
|
110
|
+
|
|
111
|
+
// At setup: tag the entities (or declare Tag in main-entities.ts).
|
|
112
|
+
for (const barrel of [b1, b2, b3]) {
|
|
113
|
+
Tag.createOrReplace(barrel, { value: 'breakable' })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
engine.addSystem(() => {
|
|
117
|
+
for (const [entity] of engine.getEntitiesByTag('breakable')) {
|
|
118
|
+
const cmd = inputSystem.getInputCommand(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN, entity)
|
|
119
|
+
if (cmd) {
|
|
120
|
+
// entity was the IA_PRIMARY target this frame
|
|
121
|
+
engine.removeEntity(entity)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Cleaner than registering N individual `pointerEventsSystem.onPointerDown` handlers when the behavior is uniform.
|
|
128
|
+
|
|
102
129
|
## All InputAction Values
|
|
103
130
|
|
|
104
131
|
| InputAction | Key/Button |
|
|
@@ -138,11 +165,15 @@ InputModifier.create(engine.PlayerEntity, {
|
|
|
138
165
|
mode: InputModifier.Mode.Standard({ disableAll: true })
|
|
139
166
|
})
|
|
140
167
|
|
|
141
|
-
// Restrict specific movement
|
|
168
|
+
// Restrict specific movement (every flag is optional and defaults to false)
|
|
142
169
|
InputModifier.createOrReplace(engine.PlayerEntity, {
|
|
143
170
|
mode: InputModifier.Mode.Standard({
|
|
171
|
+
disableWalk: false,
|
|
172
|
+
disableJog: false,
|
|
144
173
|
disableRun: true,
|
|
145
174
|
disableJump: true,
|
|
175
|
+
disableDoubleJump: true,
|
|
176
|
+
disableGliding: true,
|
|
146
177
|
disableEmote: true
|
|
147
178
|
})
|
|
148
179
|
})
|
|
@@ -151,6 +182,8 @@ InputModifier.createOrReplace(engine.PlayerEntity, {
|
|
|
151
182
|
InputModifier.deleteFrom(engine.PlayerEntity)
|
|
152
183
|
```
|
|
153
184
|
|
|
185
|
+
While restricted: gravity still applies, the camera still rotates, and pointer / proximity events still fire. All restrictions auto-lift when the player leaves the scene.
|
|
186
|
+
|
|
154
187
|
**Important:** InputModifier only works in the DCL 2.0 desktop client. It has no effect in the web browser explorer.
|
|
155
188
|
|
|
156
189
|
### Cutscene Pattern
|
|
@@ -5,6 +5,22 @@ description: Advanced rendering in Decentraland scenes. Billboard (face camera),
|
|
|
5
5
|
|
|
6
6
|
# Advanced Rendering in Decentraland
|
|
7
7
|
|
|
8
|
+
## Authoring split
|
|
9
|
+
|
|
10
|
+
`Billboard`, `TextShape`, `Material`, `MeshRenderer`, `GltfContainer`, `VisibilityComponent`, and `GltfNodeModifiers` are **all supported in `main-entities.ts`** — declare the visual entity (sign, label, billboard, glowing prop, model with per-node overrides) fully there with its visual components. Examples below that show `engine.addEntity()` followed by `Transform.create` + `MeshRenderer` / `Billboard` / `TextShape` are pre-`main-entities.ts` patterns — translate them by moving the entity declaration into `main-entities.ts` and keeping only runtime modifications (e.g., `VisibilityComponent.getMutable(entity).visible = false`) in `src/index.ts`.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// Example: floating label
|
|
14
|
+
// main-entities.ts
|
|
15
|
+
shop_label: {
|
|
16
|
+
components: {
|
|
17
|
+
Transform: { position: { x: 8, y: 3, z: 8 } },
|
|
18
|
+
TextShape: { text: 'OPEN', fontSize: 4, textColor: { r: 1, g: 1, b: 1, a: 1 } },
|
|
19
|
+
Billboard: { billboardMode: 7 } // BM_ALL
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
8
24
|
## When to Use Which Rendering Feature
|
|
9
25
|
|
|
10
26
|
| Need | Component | When |
|
|
@@ -230,21 +246,78 @@ engine.addSystem(lodSystem)
|
|
|
230
246
|
|
|
231
247
|
### Per-Node Modifiers (GltfNodeModifiers)
|
|
232
248
|
|
|
233
|
-
Override material or shadow casting on specific nodes within a GLTF model
|
|
249
|
+
Override material or shadow casting on specific nodes within a GLTF model. Supported in `main-entities.ts`:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// main-entities.ts
|
|
253
|
+
armored_knight: {
|
|
254
|
+
components: {
|
|
255
|
+
Transform: { position: { x: 8, y: 0, z: 8 } },
|
|
256
|
+
GltfContainer: { src: 'models/knight.glb' },
|
|
257
|
+
GltfNodeModifiers: {
|
|
258
|
+
modifiers: [
|
|
259
|
+
{
|
|
260
|
+
path: 'RootNode/Armor', // GLTF hierarchy path
|
|
261
|
+
castShadows: false // disable shadows for this node
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Whole-model override**: pass `path: ''` (empty string) to apply the modifier to every node in the model. Useful for re-skinning an entire model with a single material swap:
|
|
234
270
|
|
|
235
271
|
```typescript
|
|
236
|
-
|
|
272
|
+
red_team_unit: {
|
|
273
|
+
components: {
|
|
274
|
+
Transform: { position: { x: 4, y: 0, z: 8 } },
|
|
275
|
+
GltfContainer: { src: 'models/unit.glb' },
|
|
276
|
+
GltfNodeModifiers: {
|
|
277
|
+
modifiers: [
|
|
278
|
+
{
|
|
279
|
+
path: '',
|
|
280
|
+
material: {
|
|
281
|
+
material: {
|
|
282
|
+
$case: 'pbr',
|
|
283
|
+
pbr: { albedoColor: { r: 1, g: 0, b: 0, a: 1 } }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Texture Tweens (Animate Material Textures)
|
|
294
|
+
|
|
295
|
+
Animate a material's texture offset/tiling over time — useful for water, lava, conveyor belts, scrolling backgrounds. Tween component is supported in `main-entities.ts`:
|
|
237
296
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
297
|
+
```typescript
|
|
298
|
+
// main-entities.ts — continuous texture scroll
|
|
299
|
+
conveyor: {
|
|
300
|
+
components: {
|
|
301
|
+
Transform: { position: { x: 8, y: 1, z: 8 } },
|
|
302
|
+
MeshRenderer: { mesh: { $case: 'plane', plane: { uvs: [] } } },
|
|
303
|
+
Material: { material: { $case: 'pbr', pbr: { texture: { tex: { $case: 'texture', texture: { src: 'images/belt.png' } } } } } },
|
|
304
|
+
Tween: {
|
|
305
|
+
duration: 2000,
|
|
306
|
+
easingFunction: 0, // EF_LINEAR
|
|
307
|
+
mode: {
|
|
308
|
+
$case: 'textureMoveContinuous',
|
|
309
|
+
textureMoveContinuous: { direction: { x: 1, y: 0 }, speed: 0.5 }
|
|
310
|
+
// movementType defaults to 0 = TMT_OFFSET; use 1 = TMT_TILING to scale instead
|
|
311
|
+
}
|
|
243
312
|
}
|
|
244
|
-
|
|
245
|
-
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
246
315
|
```
|
|
247
316
|
|
|
317
|
+
For a finite from-to texture move, use `{ $case: 'textureMove', textureMove: { start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, movementType: 0 } }` with a `duration`.
|
|
318
|
+
|
|
319
|
+
Runtime helpers (in `src/index.ts`): `Tween.setTextureMove(entity, from, to, durationMs)` and `Tween.setTextureMoveContinuous(entity, direction, speed)`.
|
|
320
|
+
|
|
248
321
|
### Avatar Texture
|
|
249
322
|
|
|
250
323
|
Generate a texture from a player's avatar:
|