LeCodesdocs

Orbit

A few lit primitives on a ground plane, one sun casting soft shadows, and a camera you orbit by dragging. Shadows need three things to agree — the sun's shadowsQuality, castShadows on each mesh, and receiveShadows on the ground; ambient fill comes from the scene's IBL. The drag tracks a pointer with ev.track() and moves the camera on a sphere around a fixed target.

Fork in LeCodes
// Hello, 3D: a few lit primitives on a ground plane, one sun casting soft shadows, and a camera
// you orbit by dragging. Shadows need three things to agree — the sun's shadowsQuality, castShadows
// on each caster, and receiveShadows on the ground. Ambient fill comes from the scene's IBL.
const scene = new Scene({ skybox: "#0e1116", antialias: true })
scene.add(Light.sun({ direction: [-1, -2.2, -1], intensity: 90000, shadowsQuality: 2 }))

scene.add(Mesh.plane({
    material: Material.lit({ color: "#20242e" }),
    normal: [0, 1, 0], scale: 24, receiveShadows: true,
}))

const lit = (color: string) => Material.lit({ color })
scene.add(Mesh.box({ material: lit("#FF4032"), position: [-1.5, 0.5, 0.2], castShadows: true }))
scene.add(Mesh.sphere({ material: lit("#38bdf8"), radius: 0.65, position: [0.3, 0.65, 0.4], castShadows: true }))
scene.add(Mesh.cylinder({ material: lit("#FFB020"), position: [1.7, 0.5, -0.5], castShadows: true }))

// Camera orbit: spherical coordinates around a fixed target. A drag adds to the yaw/pitch captured
// at touch-down; pitch is clamped so the camera never flips under the floor or straight overhead.
const target = [0, 0.55, 0]
let yaw = 0.7, pitch = 0.5
const RADIUS = 6.5

function place() {
    scene.camera.position = [
        target[0] + RADIUS * Math.cos(pitch) * Math.sin(yaw),
        target[1] + RADIUS * Math.sin(pitch),
        target[2] + RADIUS * Math.cos(pitch) * Math.cos(yaw),
    ]
    scene.camera.lookAt(target)
}
place()

scene.addEventListener("touchstart", ev => {
    const yaw0 = yaw, pitch0 = pitch
    ev.track({
        claim: true,
        onMove({ clientX, clientY }) {
            yaw = yaw0 - (clientX - ev.clientX) * 0.01
            pitch = Mathf.clamp(pitch0 + (clientY - ev.clientY) * 0.01, 0.15, 1.45)
            place()
        },
    })
})

// A HUD over a scene is a UIWidget, never a UIScreen — a screen and a scene can't be open at the
// same time. attachTo(scene) binds the widget to the scene's layer so it shows and hides with it.
const hud = UIWidget([
    UIText("Drag to orbit").style({ fontSize: 14, color: "#c7ccd6", fontWeight: 600 }),
]).style({ top: "max(safe-top, 16px)", left: 16 })
hud.attachTo(scene)

scene.open()
hud.show()

Press Run to launch the floating simulator. It stays open as you browse — opening another example just swaps the app inside it.

Related docs