Material Conventions
These conventions apply to all meshes entering the scene — via
b3d-loader, b3d-library, or any
component that calls register().
Property-Based (automatic from Blender materials)
| Property | Threshold | Effect |
|---|---|---|
alpha > 0.95 |
snapped to 1.0 | Treated as fully opaque (avoids blend cost) |
alpha ≤ 0.95 |
— | Alpha blend, depth pre-pass, double-sided, excluded from shadow casting |
unlit (glTF KHR_materials_unlit) |
— | Respected as-is |
Transmission > 0 + _mirror |
— | Cubemap-based refraction with proper IOR (replaces glTF screen-space) |
Name Suffixes (behavioral overrides, not material appearance)
| Suffix | Effect |
|---|---|
_noshadow / -noshadow |
Mesh doesn't receive shadows |
_nocast / -nocast |
Mesh doesn't cast shadows |
_mirror / -mirror |
Dynamic reflection probe (+ refraction if transmissive) |
-ignore |
Node is disposed on load |
_collide* |
Physics collider (sphere/box/cylinder/mesh) |
B3dChild
B3dChild is the base class for any custom element you write that lives inside a
<tosi-b3d> scene. It owns the whole scene-attach lifecycle in one place so you
never have to reason about component-vs-scene timing yourself.
Extend it (or AbstractMesh, which extends B3dChild and adds
position/rotation syncing) and override two hooks:
| Hook | When it runs | What to do |
|---|---|---|
sceneReady(owner, scene) |
Once, when both this element is connected (its attributes are drained and readable) and the scene's engine/scene exist. | Build your Babylon content, owner.register({meshes, lights}), subscribe to owner.onSceneAddition(...). |
sceneDispose() |
On disconnect (element removed, or the whole scene torn down). | Dispose meshes/materials, unsubscribe, release references. |
Do not override connectedCallback / disconnectedCallback — the pull-model
plumbing is centralized in B3dChild so a lifecycle fix lands in exactly one spot.
(If you must, call super first and keep it side-effect-free.)
Why a base class rather than parent orchestration? On connect the child discovers its
owner via findB3dOwner and asks to attach: owner.whenReady(cb) runs
cb immediately if the scene is already up, otherwise the instant it becomes ready.
The parent never pushes sceneReady at a guessed moment, so sceneReady fires only
when the child is genuinely ready AND the scene exists — no races against tosijs's
deferred attribute application.
import { B3dChild } from 'tosijs-3d'
import * as BABYLON from '@babylonjs/core'
class MyThing extends B3dChild {
static initAttributes = { color: '#ff8800' }
declare color: string
mesh?: BABYLON.Mesh
sceneReady(owner, scene) {
this.mesh = BABYLON.MeshBuilder.CreateBox('mything', {}, scene)
owner.register({ meshes: [this.mesh] })
}
sceneDispose() {
this.mesh?.dispose()
this.mesh = undefined
}
}
export const myThing = MyThing.elementCreator({ tag: 'my-thing' })