Rotation quaternion relative to the parent, or nil once the node is removed. Assign a quat; a physics body follows at the next physics step.
restitution
number?
read/write
Bounciness, 0 or more (0 absorbs an impact, 1 keeps all its energy); nil if the handle has no physics body. Assign a number to change it at runtime.
friction
number?
read/write
Surface friction coefficient; nil if the handle has no physics body. Assign a number to change it at runtime.
ccd
boolean
read/write
Whether this body, when moving fast, also sweeps moving (kinematic and dynamic) bodies so it never tunnels through them; fast bodies always sweep static geometry. Assign a boolean to toggle.
visible
boolean
read/write
Whether the node is shown. Setting false hides it and every node parented under it; true defers to the parent's visibility.
The node this node is parented to (its transform composes with the parent's), or nil if it sits at the scene root. Assign a node to reparent (the local transform is kept, and a physics body moves with the node), or nil to detach; reparenting across scenes, or under the node itself or one of its descendants, raises.
Camera-facing mode. While not none, turning to face the camera sets the node's rotation every frame, replacing any rotation assigned to it, and a physics body on the sprite keeps its own.
Base color. On sprites, a tint multiplied with the texture; on mesh primitives, the material's base color. Writing it changes this node alone: a node sharing a material takes its own copy, which later edits to the shared material no longer reach.
flip_x
boolean
read/write
Mirror horizontally.
flip_y
boolean
read/write
Mirror vertically.
shaded
boolean
read/write
Lit by scene lights when true; full-bright (unlit) when false.
cell
integer
read/write
Current atlas cell index (0-based, row-major). Raises if the sprite draws a whole image.
Update node transform / material / light properties.
Example
-- Only the keys given change; the others keep their values.
node:update({
pos = vec3(2, 0.5, 0),
color = datumhue.color("#e67e80"),
})
print(node.pos) --> vec3(2.0, 0.5, 0.0)
optsscene.NodeUpdateOptionsoptions11 keys, 3 inheritedOmitted keys keep their current value.
Target color, blended perceptually (Oklab, like Color:lerp). The node gets its own copy of its material, so other nodes sharing it are unaffected; a light tweens its light color instead. Raises on a node that shows a shader unlit, whose colors are its uniforms.
opacity
number
the opacity is not tweened
Target alpha 0.0-1.0 for the material's base color. The node's material copy switches to alpha blending if it was opaque. Raises on a node that shows a shader unlit.
Play several of the model's clips at once at the given weights (a blend tree), replacing whatever was playing. Each clip loops. Only meaningful on nodes created with scene:model.
Example
local asset = datumhue.package:dir():read("robot.glb"):scene()
local robot = scene:model({ asset = asset })
-- Weights are relative: these play at 75 % and 25 %.
robot:blend({
{ clip = "Animation0", weight = 3 },
{ clip = "Animation1", weight = 1 },
})
The mesh's local-space bounding box (intrinsic dimensions, before the node's transform). nil for nodes without a direct mesh (loaded models, lights, groups).
Attach a deformer to this node's mesh — it displaces the mesh every frame without running any Lua. One deformer per node: a second call changes the kind and the parameters it gives, and keeps the original base geometry. mesh:update with new positions re-bases an attached deformer. scene:raycast hits the displaced mesh; a physics body keeps the primitive's own shape.
Example
local water = scene:plane({ scale = vec3(6, 1, 6), subdivisions = 32 })
-- Lengths are in the plane's own units, before its scale.local waves = water:deform({
kind = "wave",
amplitude = 0.1,
frequency = 2,
speed = 2,
direction = datumhue.math.vec2(1, 0),
})
Attach a physics body. A DrawPrimitive's body simulates in the app's 2D world, a SceneNode's in its 3D world. See physics.BodyOptions for the shape + material fields. With no shape, the collider fits what the handle draws, measured when add_body runs: a draw primitive's drawing (a circle for a round one, a text's laid-out block, a group's children combined), or a scene node's mesh (a plane is a thin slab under it, a torus the disc it spans, a billboard sprite an upright cylinder standing on its pivot, anything else a unit box). A draw primitive's named shape also takes omitted sizes from its drawing; a scene node's takes width, height, and depth 1 and radius 0.5. The collider, unless built from points, is centered on the drawing; refit_body measures again. Sizes and points are in the handle's own units: the collider scales and mirrors with the handle and every group or node above it, except that a sprite sized in pixels keeps its unscaled collider. A size or point, or a collider reaching, past 1000000 units raises. The body follows whatever moves, turns, or scales the handle or anything above it at the next physics step: a static or kinematic body goes where its handle goes; a dynamic body simulates on its own and jumps, keeping its velocity, when your code moves it or something above it. Replacing a body keeps its joints. Raises, keeping any previous body, for a text span or a voxel volume, a negative or non-finite size, a non-finite point, a triangle naming a point that does not exist, or a world other than the jointed body's own.
Example
local floor =
scene:cube({ pos = vec3(0, -0.5, 0), scale = vec3(10, 1, 10) })
floor:add_body({ type = "static" })
-- With no shape, the collider fits the mesh as drawn.
node:add_body({ restitution = 0.8 })
local ball = scene:sphere({ pos = vec3(1, 3, 0) })
ball:add_body({ shape = "sphere", radius = 0.5, mass = 2 })
Box, cylinder, or cone height, or a capsule's straight length between its caps, in the handle's own units.
depth
number
taken from what the handle draws, or 1
Box depth in the handle's own units, 3D only.
radius
number
taken from what the handle draws, or 0.5
Circle, sphere, capsule, cylinder, or cone radius, in the handle's own units.
restitution
number
0.3
Bounciness, 0 or more: 0 absorbs an impact, 1 keeps all its energy.
friction
number
0.5
Surface friction, 0 or more.
mass
number
derived from the collider's size
Mass override, more than 0.
sensor
boolean
false
Make the body a non-colliding trigger volume that reports the bodies overlapping it: dynamic and kinematic ones, and static ones unless the sensor is static itself.
Axes to lock rotation on, on top of a 2D body's in-plane constraints.
ccd
boolean
false
Sweep this body against moving (kinematic and dynamic) bodies too when it moves fast; fast bodies always sweep static geometry so they never tunnel through it.
Attach a kinematic character controller to this body and return its CharacterController handle. The body must be kinematic. The controller solves walking/sliding/stair-stepping geometry only — gravity, jump, input, and camera stay in your code. Lengths, absolute or relative, are 0 to 1000000. Raises if the body is missing, not kinematic, or already has a controller (remove() to rebind).
Example
-- The body must be kinematic; lengths are in scene units.
node:add_body({ type = "kinematic" })
local walker = node:character_controller({
max_slope_climb_angle = math.rad(50),
snap_to_ground = 0.2,
autostep = { max_height = 0.3, min_width = 0.2 },
})
Step-climbing config, or false to disable; stepping is expensive. Needs a flat-bottomed body (cube or cylinder); a capsule's rounded bottom slides back off the ledge. The body also needs head room of its own height plus max_height above where it stands, so a low ceiling stops it stepping.
normal_nudge_factor
number
0.0001
Tiny anti-stick push along contact normals while sliding, 0 to 1.
Lock or unlock individual translation/rotation axes at runtime. The 2D in-plane constraints are always preserved.
Example
node:add_body()
-- Slide in the XZ plane only, and turn only about Y.
node:set_locked_axes({
lock_translation = { "y" },
lock_rotation = { "x", "z" },
})
Set, replace, or clear (pass nil) the handler fired when this body starts colliding. contact is nil when either body is a sensor. Not fired once this body's own handle has been removed, even by an earlier collision handler in the same step; other may have been removed that way, and its handle still compares equal to one you hold.
Set, replace, or clear (pass nil) the handler fired when this body stops colliding. Removing either body of a touching pair (its handle, or only its body, as remove_body does) ends the contact on the next physics step; other may be the removed body, and its handle still compares equal to one you hold. Not fired once this body's own handle has been removed, even by an earlier collision handler in the same step.
SceneNode:remove_body
method
SceneNode:remove_body()
Remove all physics from this handle.
SceneNode:refit_body
method
SceneNode:refit_body()
Measure what the handle draws again and resize its collider to fit, in place: velocity, joints, and contacts carry on. Sizes given to add_body stay; the ones it took from the drawing, and the collider's center, follow the drawing as it is now (a text laid out again, a group whose children moved). Raises, keeping the collider, for a handle with no body, a voxel volume (its body follows its cells), or a collider that would reach more than 1000000 units from the body.