Skip to main content

Lighting: Three-Point Lighting

A single 3D subject lit from three directions — a DirectionalLight key, a PointLight fill, and a SpotLight rim — plus an EnvironmentLight for the global ambient wash. Tap anywhere to cycle through four preset combinations: Key only, Classic 3-Point, Soft Point fill, and Rim only. Each setup flips the per-light SceneObjects on/off via setEnabledInHierarchy(...); the lights themselves are never deleted or recreated.

Three-Point Lighting demo running in Effect House preview

What you'll build

  • Three custom lights placed around a sphere subject:
    • KeyLightDirectionalLight at (8, 6, 8) aimed back at the subject with localEulerAngles = (-25, 35, 0). The dominant front-right key.
    • FillLightPointLight at (-6, 1, 4). Soft, omnidirectional spill from the camera-left side that lifts the shadow.
    • BackLightSpotLight at (0, 4, -6) with localEulerAngles = (30, 180, 0). A focused rim from behind that separates the subject from the background.
  • An EnvironmentLight kept on across all setups for a baseline ambient wash — without it, "Rim only" reads as nearly black.
  • The auto-created default Directional Light is hidden (setEnabledInHierarchy(false)) so the demo's lights are the only contributors.
  • A TitleText and StatusText 2D HUD that mirrors the active setup.
  • A GameController empty SceneObject hosting LightingDirector — the script that subscribes to global EventType.Touch and toggles the right pattern on each tap.

Open the demo

↓ three-point-lighting.zip

Unzip and open in Effect House (5.9.0+). The opening scene contains:

  • Camera — default 3D perspective camera at (0, 0, 14) looking at the world origin.
  • Subject — a Sphere at the origin with localScale = (0.4, 0.4, 0.4) so it reads at a comfortable size against the default phone framing.
  • Directional Light (auto-created) — disabled in the inspector (enabled: false). We don't delete it; we just leave it dormant.
  • Environment Light — enabled, contributes the soft global ambient baseline.
  • KeyLight / FillLight / BackLight — the three demo lights, each with its own Transform and matching light component.
  • TitleText — "Lighting: 3-Point Demo" at the top.
  • StatusText<n> of 4 plus the setup name, near the bottom.
  • GameController — empty SceneObject hosting LightingDirector.

Read the script

LightingDirector.ts

@component()
export class LightingDirector extends APJS.BasicScriptComponent {
// The three custom lights placed around the subject. We drive each by
// toggling its SceneObject's enabled state — this is the simplest way to
// turn a scene light "off" at runtime.
@serializeProperty keyLight!: APJS.SceneObject; // DirectionalLight, front-right
@serializeProperty fillLight!: APJS.SceneObject; // PointLight, soft fill from left
@serializeProperty backLight!: APJS.SceneObject; // SpotLight, rim from behind

// The status label that mirrors the current setup name.
@serializeProperty statusText!: APJS.SceneObject;

// Setup index. We start at 1 = the canonical "Classic 3-Point" so the
// initial preview reads as the recognizable reference.
@serializeProperty startSetup: number = 1;

private statusComp!: APJS.Text;
private touchCallback!: (e: APJS.IEvent) => void;
private cycleIndex: number = -1;
private inited: boolean = false;

// Each setup is a 3-bit pattern over [key, fill, back]: true = on, false = off.
// 0 — Key Only: dramatic single-source, hard shadows on the unlit side.
// 1 — Classic 3-Point: industry-standard portrait setup, key + soft fill + rim.
// 2 — Soft Point: no key, no rim — gentle round-the-side wash from the fill.
// 3 — Rim Only: only the spot from behind — a silhouette / hero rim look.
private static readonly SETUPS: { name: string; key: boolean; fill: boolean; back: boolean }[] = [
{ name: "Key only", key: true, fill: false, back: false },
{ name: "Classic 3-Point", key: true, fill: true, back: true },
{ name: "Soft Point fill", key: false, fill: true, back: false },
{ name: "Rim only", key: false, fill: false, back: true },
];

onUpdate(_dt: number): void {
if (this.inited) return;
if (!this.keyLight || !this.fillLight || !this.backLight || !this.statusText) return;

this.statusComp = this.statusText.getComponent("Text") as APJS.Text;

this.touchCallback = (event: APJS.IEvent) => {
const t = event.args[0] as APJS.TouchData;
if (t.phase !== APJS.TouchPhase.Began) return;
this.advance();
};
APJS.EventManager.getGlobalEmitter()
.on(APJS.EventType.Touch, this.touchCallback, this);

// Apply the starting setup immediately so the preview reads correctly
// before the user taps anything.
this.cycleIndex = ((this.startSetup % LightingDirector.SETUPS.length) + LightingDirector.SETUPS.length) % LightingDirector.SETUPS.length - 1;
this.advance();

this.inited = true;
console.log("[LightingDirector] ready — starting setup " + this.cycleIndex);
}

onDestroy(): void {
if (this.touchCallback) {
APJS.EventManager.getGlobalEmitter()
.off(APJS.EventType.Touch, this.touchCallback, this);
}
}

private advance(): void {
this.cycleIndex = (this.cycleIndex + 1) % LightingDirector.SETUPS.length;
const s = LightingDirector.SETUPS[this.cycleIndex];

// setEnabledInHierarchy(false) hides the SceneObject AND every component
// it carries — in this case the DirectionalLight / PointLight / SpotLight
// each stop contributing to the lighting solution. Flipping it back on
// re-engages the light without any per-component setup.
this.keyLight.setEnabledInHierarchy(s.key);
this.fillLight.setEnabledInHierarchy(s.fill);
this.backLight.setEnabledInHierarchy(s.back);

this.statusComp.text = (this.cycleIndex + 1) + " of " + LightingDirector.SETUPS.length + "\n" + s.name;
console.log("[LightingDirector] setup " + s.name);
}
}

The Lighting-namespace calls of interest:

  • DirectionalLight — a parallel light source used for the KeyLight. Position is irrelevant; only the SceneObject's rotation (Transform.localEulerAngles) changes the direction the light points. Cheap, suitable for an outdoor "sun"-style key.
  • PointLight — an omnidirectional light that emits in all directions from Transform.localPosition. Used for the soft FillLight because its falloff naturally produces a gentle round-the-side wash — no aiming required.
  • SpotLight — a cone-shaped light that does have a direction. Used for the BackLight because we want the rim to land only on the back of the subject without spilling into the scene around it. Both localPosition and localEulerAngles matter here.
  • EnvironmentLight — the global ambient wash. We keep it on in every setup so that "Rim only" still has enough fill to read the silhouette; turning it off too produces a near-black frame.
  • SceneObject.setEnabledInHierarchy(true|false) — the runtime toggle. It hides the SceneObject and its components, so the attached light component stops contributing to the lighting solution as soon as the flag flips false. Re-enabling restores the light without any component re-creation.

Cycle table

IndexNameKey (Directional)Fill (Point)Back (Spot)
0Key onlyonoffoff
1Classic 3-Pointononon
2Soft Point filloffonoff
3Rim onlyoffoffon

Each row reads the same physical scene — only setEnabledInHierarchy differs.

Customize

On GameControllerLightingDirector:

  • keyLight / fillLight / backLight — drop in any light-bearing SceneObjects. The script doesn't care what type each light component is; toggling the SceneObject disables whatever Light subclass lives on it.
  • startSetup — index 0–3 (matches the table above). The script decrements then re-runs advance(), so the first frame already shows the chosen preset. Defaults to 1 ("Classic 3-Point").
  • SETUPS — a static readonly table at the top of the script. Add or remove rows to change the cycle; modulo-wrap kicks in automatically. Bigger arrays = longer cycle.

In the editor, on each light SceneObject:

  • Transform.localPosition — moves the light in world space. For point/spot, position is everything. For directional, only rotation matters.
  • Transform.localEulerAngles — aims the directional and spot lights. The demo's key sits at (-25, 35, 0) (pitched down, yawed right toward the subject); the back-spot at (30, 180, 0) (slightly down, fully reversed).
  • Light component intensity / color / range — tune in the inspector to push from a soft portrait look to a high-contrast cinema look. Halving EnvironmentLight intensity makes "Rim only" much more dramatic.

Suggestions for further play:

  • Swap the cycle for an A/B comparison: bind GestureType.Tap to preset 1 and GestureType.LongTap to preset 0 (see the Events & Input tutorial) so the user can hold to compare hard key vs. soft fill.
  • Animate Transform.localEulerAngles of the key light over time so the "sun" sweeps across the subject — combine with Tween (see the Math tutorial for rotation patterns).
  • Layer the Lighting setup with Portrait Segmentation so the same three-point rig lights only the user's foreground silhouette, with a different background environment. Camera-feed-first rules apply.

What you learned

This tutorial used:

  • DirectionalLight — infinite parallel light, aimed by Transform rotation. Used for the dominant key.
  • PointLight — omnidirectional, positional. Used for the soft fill.
  • SpotLight — cone-shaped, position + rotation matter. Used for the rim.
  • EnvironmentLight — global ambient wash. Kept on across all setups so the "Rim only" preset still has enough fill to read.
  • SceneObject.setEnabledInHierarchy(true|false) — the runtime on/off toggle for whole light SceneObjects, including their components. The cheapest way to swap lighting at runtime.
  • @serializeProperty SceneObject × 4 for the wired light slots
    • status text — wired in the inspector, mapped to a runtime Text component lookup in the lazy-init onUpdate.

Read the full DirectionalLight reference, the PointLight reference, the SpotLight reference, the EnvironmentLight reference, the Light reference, and the Lighting namespace overview.

For the touch-event subscription pattern used here, see the Events & Input tutorial. For swapping in a per-tap gesture variant (long-press, drag-to-aim), see GestureType.

Copyright © 2026 TikTok. All rights reserved.
About TikTokHelp CenterCareersContactLegalTerms of ServicePrivacy PolicyCookies