Docs

Light and arrange a scene

Draw a scene in the light of a time of day, change its haze, and let a person move, turn and scale an object with handles.

How a scene looks and where its objects stand are two different things. The light is a SceneEnvironment, plain data the renderer draws; where an object stands is world state, and only scene.update changes it. The handles of the three.js renderer report a move, a turn or a scale; your code decides whether it happens.

arrange-view.tsx
"use client";

import { useMemo, useState } from "react";
import { environmentOf, type EnvironmentPreset } from "@klorad/api/world";
import type { TransformMode } from "@klorad/engine-three";
import { ThreeView } from "@klorad/engine-three/react";
import { useScene } from "@klorad/react";

/** A view in the light of a time of day, whose selected object a person moves, turns and scales. */
export function ArrangeView() {
  const scene = useScene();
  const [preset, setPreset] = useState<EnvironmentPreset>("evening");
  const [fog, setFog] = useState(0.2);
  const [selected, setSelected] = useState<string | null>(null);
  const [mode, setMode] = useState<TransformMode | null>("move");
  // A preset is a starting point; the environment is plain data, so adjust it by making a new value.
  const environment = useMemo(() => ({ ...environmentOf(preset), fog }), [preset, fog]);
  return (
    <>
      <ThreeView
        environment={environment}
        selected={selected}
        onPick={setSelected}
        transform={mode}
        onTransform={({ objectId, position, heading, scale }) => scene.update(objectId, { position, orientation: { heading }, scale })}
      />
      <button onClick={() => setPreset("night")}>Night</button>
      <input type="range" min={0} max={1} step={0.05} value={fog} onChange={(e) => setFog(Number(e.target.value))} />
      <button onClick={() => setMode("turn")}>Turn</button>
      <button onClick={() => setMode(null)}>Done</button>
    </>
  );
}

The light of a time of day

environmentOf(preset) gives the environment of one of the four presets in ENVIRONMENT_PRESETS: "morning", "day", "evening" and "night". Each holds what the renderer draws: the sky colour behind the scene, the sun (an azimuth in degrees clockwise from north, an elevation above the horizon, an intensity and a color), ambient light, fog from 0 (clear) to 1 (thick), whether the ground sheet is drawn, and an overall exposure.

A preset is a starting point. To adjust one, make a new value from it, as the example does with the haze, and pass it as environment to ThreeView (or to createThreeRenderer). A renderer made without one keeps its own even light; setEnvironment(environment) on a renderer changes it later, and setEnvironment(undefined) goes back to that light. No object changes with the light.

checkEnvironment(value) lists every problem of an environment, such as a sky that is not a colour like #9cc3e4 or a fog above 1. Check one that comes from outside your code (a form, a stored file) before you draw it.

Handles that move, turn and scale

Pass onTransform and the view can show handles on the selected object; transform says which: "move", "turn", "scale", or null for none. With createThreeRenderer, make the renderer with onTransform and call setTransform(objectId, mode); it throws on a renderer made without onTransform.

Each report is an ObjectTransform: the objectId, the position the object would stand at in the scene's frame, its heading in degrees clockwise from north, its scale, and the phase of the gesture ("start", "move", "end"). A turn is only about the up axis, so it is a heading; a scale keeps the object's proportions.

Like a drag, the handles move nothing in the world. Update the scene with what they report and the change becomes one event of the scene, seen by every view and every hook:

scene.update(objectId, { position, orientation: { heading }, scale });

To refuse a change, such as a piece that is fixed in place, ignore the report: the object stays where the scene keeps it, and the handles go back to it once they are let go.

Publishing what you arranged

A public World can carry the light and the arrangement too. publicPackage takes a PublicPresentation as its fifth argument: the environment, the fields that say what a property means, and per object a name, a placement (east, north, up, heading, scale), the values of described properties and whether it is hidden. It is applied over the sanitised projection, so it can hide and arrange public objects and add only the properties fields describes; it never brings back what the filter keeps out. checkPresentation lists its problems first.

presentation.ts
import { checkPresentation, createScene, environmentOf, packScene, publicPackage, type PublicationFilter, type PublicPresentation } from "@klorad/api/world";

const scene = createScene({ name: "Courtyard", coordinateSystem: { origin: { lat: 40.632, lon: 22.951, height: 12 } } });
scene.add({ id: "stele", name: "Stele", position: { east: 0, north: 4 }, geometry: { kind: "box", width: 0.8, depth: 0.3, height: 2.2 }, properties: { kind: "exhibit" } });
scene.add({ id: "column", name: "Column", position: { east: 0, north: -4 }, geometry: { kind: "cylinder", radius: 0.35, height: 3 }, properties: { kind: "exhibit" } });

const filter: PublicationFilter = { properties: ["kind"], quantities: [], alarms: [] };

// How visitors see it: the light, what a property means, and where each piece stands for them.
const presentation: PublicPresentation = {
  environment: environmentOf("evening"),
  fields: [{ key: "period", label: "Period", type: "period" }],
  objects: {
    stele: { name: "Funerary stele", heading: 15, properties: { period: "4th century BC" } },
    column: { hidden: true },
  },
};

export async function publishCourtyard() {
  const problems = checkPresentation(presentation);
  if (problems.length) throw new Error(problems.join("; "));
  const pub = await publicPackage(await packScene(scene), filter, "Courtyard", undefined, presentation);
  pub.chunks.flatMap((c) => c.objects).map((o) => o.name); // ["Funerary stele"]: the column is kept out
  pub.manifest.environment?.preset; // "evening"
  return pub;
}

See Publish a public World for the filter itself.

Run it liveSandbox: Light and arrangeA courtyard seen at four times of day, its pieces moved, turned and scaled with handles, each change an update of the scene.

Something wrong or unclear? Tell us.