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' })