Skip to content

Commit 1da934f

Browse files
authored
feat: SDK helpers functions added (#1275)
* feat: helper timer functions added * timer helper functions added globally, internal callbacks and timers array removed * feat: getEntitiesWithParent helper method added * feat: Material getFlat API added for simplified material property access * readonly and mutable accessors added (getFlat, getFlatOrNull, getFlatMutable, getFlatMutableOrNull) * getWorldPosition and getWorldRotation methods added * get exact transform for entity attached to the avatar * update snapshots * update snapshots
1 parent 3c16004 commit 1da934f

25 files changed

Lines changed: 3709 additions & 55 deletions

docs/material-getflat-api.md

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# Material Flat API
2+
3+
## Overview
4+
5+
The Material component provides a simplified, flattened interface for accessing and modifying Material component properties. It eliminates the need to navigate deeply nested union structures (`PBR` vs `Unlit`, `texture` vs `avatarTexture` vs `videoTexture`), making material manipulation more intuitive and less error-prone.
6+
7+
### API Methods
8+
9+
The Material component provides four flat accessor methods, following the standard ECS component pattern:
10+
11+
| Method | Returns | Missing Behavior | Use Case |
12+
|--------|---------|------------------|----------|
13+
| `getFlat(entity)` | `ReadonlyFlatMaterial` | Throws | Read-only access, component must exist |
14+
| `getFlatOrNull(entity)` | `ReadonlyFlatMaterial \| null` | Returns `null` | Read-only access, check existence |
15+
| `getFlatMutable(entity)` | `FlatMaterial` | Throws | Read/write access, component must exist |
16+
| `getFlatMutableOrNull(entity)` | `FlatMaterial \| null` | Returns `null` | Read/write access, check existence |
17+
18+
### Compile-Time Sync Validation
19+
20+
The `FlatMaterial` interface includes compile-time type assertions that ensure it stays in sync with the generated protobuf types (`PBMaterial_PbrMaterial` and `PBMaterial_UnlitMaterial`). If a new property is added to the protobuf definitions, the build will fail with a descriptive error until `FlatMaterial` is updated:
21+
22+
```typescript
23+
// Example error when 'glossiness' is added to PBMaterial_PbrMaterial but not to FlatMaterial:
24+
// Type 'boolean' is not assignable to type '{ error: "FlatMaterial is missing PBR properties"; missing: "glossiness"; }'
25+
```
26+
27+
This ensures the API stays complete as the Material schema evolves.
28+
29+
## Motivation
30+
31+
The Material component in Decentraland SDK uses a complex nested structure with discriminated unions:
32+
33+
```typescript
34+
// Before: Complex nested access pattern
35+
const mat = Material.get(entity)
36+
if (mat.material?.$case === 'pbr') {
37+
if (mat.material.pbr.texture?.tex?.$case === 'texture') {
38+
const src = mat.material.pbr.texture.tex.texture.src
39+
}
40+
}
41+
```
42+
43+
```typescript
44+
// After: Simple flat access pattern
45+
const src = Material.getFlat(entity).texture.src
46+
```
47+
48+
## API Reference
49+
50+
### `Material.getFlat(entity: Entity): ReadonlyFlatMaterial`
51+
52+
Returns a readonly `FlatMaterial` accessor object that provides direct read access to material properties. Does NOT mark the component as dirty.
53+
54+
**Throws:** Error if the entity does not have a Material component.
55+
56+
### `Material.getFlatOrNull(entity: Entity): ReadonlyFlatMaterial | null`
57+
58+
Returns a readonly `FlatMaterial` accessor object, or `null` if the entity does not have a Material component. Does NOT mark the component as dirty.
59+
60+
### `Material.getFlatMutable(entity: Entity): FlatMaterial`
61+
62+
Returns a mutable `FlatMaterial` accessor object that provides direct read/write access to material properties. Marks the component as dirty (for change detection).
63+
64+
**Throws:** Error if the entity does not have a Material component.
65+
66+
### `Material.getFlatMutableOrNull(entity: Entity): FlatMaterial | null`
67+
68+
Returns a mutable `FlatMaterial` accessor object, or `null` if the entity does not have a Material component. Marks the component as dirty if component exists.
69+
70+
### FlatMaterial Interface
71+
72+
```typescript
73+
interface FlatMaterial {
74+
// Texture accessors (all return FlatTexture)
75+
readonly texture: FlatTexture // Main texture (PBR + Unlit)
76+
readonly alphaTexture: FlatTexture // Alpha texture (PBR + Unlit)
77+
readonly emissiveTexture: FlatTexture // Emissive texture (PBR only)
78+
readonly bumpTexture: FlatTexture // Bump/normal texture (PBR only)
79+
80+
// Shared properties (PBR + Unlit)
81+
alphaTest: number | undefined // Alpha test threshold (0-1)
82+
castShadows: boolean | undefined // Whether material casts shadows
83+
84+
// PBR-only properties
85+
albedoColor: Color4 | undefined // Base color
86+
emissiveColor: Color3 | undefined // Emissive/glow color
87+
reflectivityColor: Color3 | undefined // Reflectivity color
88+
transparencyMode: MaterialTransparencyMode | undefined
89+
metallic: number | undefined // Metallic value (0-1)
90+
roughness: number | undefined // Roughness value (0-1)
91+
specularIntensity: number | undefined // Specular intensity
92+
emissiveIntensity: number | undefined // Emissive intensity
93+
directIntensity: number | undefined // Direct light intensity
94+
95+
// Unlit-only property
96+
diffuseColor: Color4 | undefined // Diffuse color (Unlit only)
97+
}
98+
```
99+
100+
### ReadonlyFlatMaterial Interface
101+
102+
The `ReadonlyFlatMaterial` interface is identical to `FlatMaterial` but with all properties marked as `readonly`, preventing mutations.
103+
104+
### FlatTexture Interface
105+
106+
```typescript
107+
interface FlatTexture {
108+
src: string | undefined
109+
wrapMode: TextureWrapMode | undefined
110+
filterMode: TextureFilterMode | undefined
111+
}
112+
```
113+
114+
### ReadonlyFlatTexture Interface
115+
116+
```typescript
117+
interface ReadonlyFlatTexture {
118+
readonly src: string | undefined
119+
readonly wrapMode: TextureWrapMode | undefined
120+
readonly filterMode: TextureFilterMode | undefined
121+
}
122+
```
123+
124+
## Usage Examples
125+
126+
### Reading Properties (Readonly Access)
127+
128+
```typescript
129+
// Read texture source - uses getFlat() for readonly access
130+
const textureSrc = Material.getFlat(entity).texture.src
131+
132+
// Read PBR properties
133+
const metallic = Material.getFlat(entity).metallic
134+
const roughness = Material.getFlat(entity).roughness
135+
const albedoColor = Material.getFlat(entity).albedoColor
136+
137+
// Read shared properties
138+
const alphaTest = Material.getFlat(entity).alphaTest
139+
const castShadows = Material.getFlat(entity).castShadows
140+
141+
// Read Unlit-only property
142+
const diffuseColor = Material.getFlat(entity).diffuseColor
143+
```
144+
145+
### Safe Reading with getFlatOrNull
146+
147+
```typescript
148+
// Check if material exists before reading
149+
const flat = Material.getFlatOrNull(entity)
150+
if (flat) {
151+
console.log('Texture:', flat.texture.src)
152+
console.log('Metallic:', flat.metallic)
153+
}
154+
```
155+
156+
### Writing Properties (Mutable Access)
157+
158+
```typescript
159+
// Update texture source - uses getFlatMutable() for write access
160+
Material.getFlatMutable(entity).texture.src = 'newTexture.png'
161+
162+
// Update texture wrap/filter modes
163+
Material.getFlatMutable(entity).texture.wrapMode = TextureWrapMode.TWM_MIRROR
164+
Material.getFlatMutable(entity).texture.filterMode = TextureFilterMode.TFM_TRILINEAR
165+
166+
// Update PBR properties
167+
Material.getFlatMutable(entity).metallic = 0.9
168+
Material.getFlatMutable(entity).roughness = 0.2
169+
Material.getFlatMutable(entity).albedoColor = { r: 1, g: 0, b: 0, a: 1 }
170+
Material.getFlatMutable(entity).transparencyMode = MaterialTransparencyMode.MTM_ALPHA_BLEND
171+
172+
// Update shared properties
173+
Material.getFlatMutable(entity).alphaTest = 0.5
174+
Material.getFlatMutable(entity).castShadows = false
175+
```
176+
177+
### Chained Updates
178+
179+
```typescript
180+
const flat = Material.getFlatMutable(entity)
181+
flat.texture.src = 'metal.png'
182+
flat.metallic = 0.95
183+
flat.roughness = 0.1
184+
flat.albedoColor = { r: 0.8, g: 0.8, b: 0.9, a: 1 }
185+
flat.castShadows = true
186+
```
187+
188+
### Safe Writing with getFlatMutableOrNull
189+
190+
```typescript
191+
// Only mutate if material exists
192+
const flat = Material.getFlatMutableOrNull(entity)
193+
if (flat) {
194+
flat.metallic = 0.9
195+
flat.roughness = 0.2
196+
}
197+
```
198+
199+
### Working with Different Textures
200+
201+
```typescript
202+
const flat = Material.getFlatMutable(entity)
203+
204+
// Main texture
205+
flat.texture.src = 'diffuse.png'
206+
207+
// Alpha texture
208+
flat.alphaTexture.src = 'alpha.png'
209+
210+
// Emissive texture (PBR only)
211+
flat.emissiveTexture.src = 'glow.png'
212+
213+
// Bump/normal texture (PBR only)
214+
flat.bumpTexture.src = 'normal.png'
215+
```
216+
217+
## Behavior Details
218+
219+
### Reading Properties
220+
221+
| Material Type | Property Type | Behavior |
222+
| ------------- | --------------------------------- | ------------------- |
223+
| PBR | PBR-only (e.g., `metallic`) | Returns the value |
224+
| PBR | Unlit-only (e.g., `diffuseColor`) | Returns `undefined` |
225+
| PBR | Shared (e.g., `alphaTest`) | Returns the value |
226+
| Unlit | PBR-only (e.g., `metallic`) | Returns `undefined` |
227+
| Unlit | Unlit-only (e.g., `diffuseColor`) | Returns the value |
228+
| Unlit | Shared (e.g., `alphaTest`) | Returns the value |
229+
230+
### Writing Properties
231+
232+
| Material Type | Property Type | Behavior |
233+
| ------------- | --------------------------------- | ---------------- |
234+
| PBR | PBR-only (e.g., `metallic`) | Sets the value |
235+
| PBR | Unlit-only (e.g., `diffuseColor`) | **Throws Error** |
236+
| PBR | Shared (e.g., `alphaTest`) | Sets the value |
237+
| Unlit | PBR-only (e.g., `metallic`) | **Throws Error** |
238+
| Unlit | Unlit-only (e.g., `diffuseColor`) | Sets the value |
239+
| Unlit | Shared (e.g., `alphaTest`) | Sets the value |
240+
241+
### Texture Handling
242+
243+
| Texture Type | Reading | Writing |
244+
| ------------------------------------------- | ------------------- | --------------------------------------- |
245+
| Regular texture (`Material.Texture.Common`) | Returns values | Sets values |
246+
| Avatar texture (`Material.Texture.Avatar`) | Returns `undefined` | **Throws Error** |
247+
| Video texture (`Material.Texture.Video`) | Returns `undefined` | **Throws Error** |
248+
| No texture set | Returns `undefined` | Creates texture structure automatically |
249+
250+
### Auto-creation of Texture Structures
251+
252+
When setting a texture property (e.g., `texture.src`) on a material that doesn't have a texture defined, the accessor automatically creates the necessary nested structure:
253+
254+
```typescript
255+
// Material created without texture
256+
Material.setPbrMaterial(entity, { metallic: 0.5 })
257+
258+
// Setting src creates the texture structure automatically
259+
Material.getFlatMutable(entity).texture.src = 'newTexture.png'
260+
261+
// Now the material has a proper texture structure
262+
```
263+
264+
## Error Handling
265+
266+
### Entity Without Material Component
267+
268+
```typescript
269+
const entity = engine.addEntity()
270+
// No Material.setPbrMaterial() or Material.setBasicMaterial() called
271+
272+
Material.getFlat(entity) // Throws: Entity does not have Material component
273+
Material.getFlatMutable(entity) // Throws: Entity does not have Material component
274+
275+
// Use OrNull variants to avoid exceptions
276+
Material.getFlatOrNull(entity) // Returns null
277+
Material.getFlatMutableOrNull(entity) // Returns null
278+
```
279+
280+
### Writing PBR Property on Unlit Material
281+
282+
```typescript
283+
Material.setBasicMaterial(entity, {})
284+
Material.getFlatMutable(entity).metallic = 0.5 // Throws: "Cannot set metallic on Unlit material. Use PBR material instead."
285+
```
286+
287+
### Writing Unlit Property on PBR Material
288+
289+
```typescript
290+
Material.setPbrMaterial(entity, {})
291+
Material.getFlatMutable(entity).diffuseColor = { r: 1, g: 0, b: 0, a: 1 } // Throws: "Cannot set diffuseColor on PBR material. Use Unlit material instead."
292+
```
293+
294+
### Modifying Special Textures
295+
296+
```typescript
297+
Material.setPbrMaterial(entity, {
298+
texture: Material.Texture.Avatar({ userId: '0xabc' })
299+
})
300+
Material.getFlatMutable(entity).texture.src = 'test.png' // Throws: "Cannot set texture properties on Avatar texture."
301+
```
302+
303+
## Implementation Notes
304+
305+
### Architecture
306+
307+
The implementation uses four accessor classes:
308+
309+
1. **`FlatMaterialAccessor`**: Implements `FlatMaterial` interface with getters/setters for all material properties (mutable).
310+
2. **`FlatTextureAccessor`**: Implements `FlatTexture` interface with getters/setters for texture properties (mutable).
311+
3. **`ReadonlyFlatMaterialAccessor`**: Implements `ReadonlyFlatMaterial` interface with only getters (readonly).
312+
4. **`ReadonlyFlatTextureAccessor`**: Implements `ReadonlyFlatTexture` interface with only getters (readonly).
313+
314+
Mutable accessors hold a reference to a `getMaterial()` function that calls `getMutable()`, marking the component as dirty.
315+
Readonly accessors use `get()` or `getOrNull()`, which does NOT mark the component as dirty.
316+
317+
### Performance Considerations
318+
319+
- The flat accessor methods create new accessor instances on each call
320+
- The accessor uses lazy evaluation - properties are only read/written when accessed
321+
- **Readonly methods (`getFlat`, `getFlatOrNull`) do NOT mark components as dirty**, making them efficient for read-only operations
322+
- For frequent updates in hot paths, consider caching the `FlatMaterial` reference:
323+
324+
```typescript
325+
// Cache the accessor if making many updates
326+
const flat = Material.getFlatMutable(entity)
327+
flat.metallic = 0.9
328+
flat.roughness = 0.1
329+
// ... more updates
330+
```
331+
332+
### Type Safety
333+
334+
- PBR-only properties return `undefined` when read on Unlit materials (no runtime error)
335+
- Writing incompatible properties throws descriptive errors at runtime
336+
- TypeScript types reflect the nullable nature of all properties
337+
- Readonly accessors prevent mutations at compile-time

docs/world-transform-no-caching.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
title: "World Transform Caching Decision"
3+
slug: "contributor/sdk/decisions/world-transform-no-caching"
4+
---
5+
6+
# World Transform: No Caching
7+
8+
This document explains the decision to **not implement caching** for the `getWorldPosition` and `getWorldRotation` helper functions in `@dcl/ecs`.
9+
10+
## Context
11+
12+
The `getWorldPosition` and `getWorldRotation` functions compute an entity's world-space position and rotation by traversing the parent hierarchy. Each call walks up the Transform chain, accumulating transformations.
13+
14+
## Current Implementation
15+
16+
The implementation uses a single internal function `getWorldTransformInternal` that computes position, rotation, and scale in **one hierarchy traversal**. This is O(depth) where depth is the number of ancestors.
17+
18+
Key characteristics:
19+
- Single-pass computation for all three transform components
20+
- Circular dependency detection via a visited set
21+
- Returns identity values for entities without Transform
22+
23+
## Why No Caching
24+
25+
### 1. Cache Invalidation Complexity
26+
27+
Transforms change frequently (animations, physics, user input). Implementing caching would require either:
28+
- Clearing the cache every frame (simple, but loses intra-frame benefits)
29+
- Tracking dirty transforms (complex, requires hooks into the component system)
30+
31+
### 2. CRDT Synchronization Risk
32+
33+
The SDK uses CRDT for network state synchronization. Remote transform updates can arrive at any point during frame execution, potentially making cached values stale mid-frame without any local indication.
34+
35+
### 3. System Ordering Issues
36+
37+
If system A modifies a parent's transform and system B later reads a child's cached world position, the cached value would be incorrect. These bugs are subtle and difficult to debug.
38+
39+
### 4. Memory Overhead
40+
41+
Storing cached world transforms for entities adds memory pressure with uncertain performance benefit, especially for scenes with many entities but shallow hierarchies.
42+
43+
### 5. Typical Usage Patterns
44+
45+
In ECS patterns, `getWorldPosition`/`getWorldRotation` are typically called once per entity per system per frame. The traversal cost is acceptable for the shallow hierarchies common in Decentraland scenes.
46+
47+
## When Caching Would Be Reconsidered
48+
49+
Caching might be worth revisiting if:
50+
- Profiling demonstrates world transform computation as an actual bottleneck
51+
- Scenes commonly use very deep hierarchies (10+ levels)
52+
- Multiple systems need the same entity's world transform per frame
53+
54+
If caching is implemented in the future, the recommended approach would be a per-frame cache explicitly invalidated at frame boundaries to avoid mid-frame staleness.
55+
56+
## Conclusion
57+
58+
The current implementation prioritizes **simplicity and correctness** over speculative optimization. The O(depth) traversal cost is acceptable for typical use cases, and avoiding caching eliminates an entire class of potential bugs related to stale data.

0 commit comments

Comments
 (0)