Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,789 changes: 1,788 additions & 1 deletion VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Hybrid.shadergraph

Large diffs are not rendered by default.

344 changes: 344 additions & 0 deletions VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertTraced.hlsl

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
uid: insert_traced_mode
title: Insert Traced Mode
description: The Insert Hybrid shader's opt-in Traced mode, which lights the body from an offline path-traced render of the insert.
---

# Insert Traced Mode

*Traced Mode* is an opt-in mode of the *Pinball/Insert Hybrid* body material. With it on, the body's glow comes from an offline, path-traced render of the insert instead of the optical response map. The bulb image and the facets' glints are still drawn live, through a facet map from the same render. With the lamp off, a second render of the insert under the room replaces the live unlit body. Off, the material is the live one, unchanged.

Everything in the [body material](body-material.md)'s *Lamp* section stays in charge: color, brightness, the lamp drive and the Insert Light component work as without Traced Mode. Traced Mode only changes where the body's light comes from.

Both pipelines draw it from the same material. HDRP shows a *Traced Mode* section after *Baked Data*, and URP shows a *Traced Mode* foldout after the graph's controls. Its controls only show while the mode is on.

## What it needs

The maps come from rendering one insert on its own, outside Unity. VPE doesn't make them yet: the [insert baker](baking.md) bakes the live mode's maps only. Import all three as linear (no sRGB), *Traced Light* and *Lamp-Off Env* as HDR (EXR), all with mipmaps so a small insert doesn't shimmer.

- *Traced Light* (`_TracedLight`): what the eye sees through each point of the top with the lamp lit, rendered from four view elevations, one per channel. A linear HDR texture.
- *Traced Facets* (`_TracedFacets`): the facet normals of the same render, X and Z in red and green, the faceted floor in alpha. It needs mipmaps.
- *Lamp-Off Env* (`_TracedEnv`): the same insert with the lamp off under the room, the four elevations in a 2×2 atlas (15°, 45° on top, 60°, 70° below), multiplied by the body mask. Optional: without it, leave *Lamp-Off Look* at zero and the unlit body stays live.

The body is shaded as a flat card at the top of the insert: the cover's refraction is undone, so the traced light doesn't move with the mesh's facets.

## Bake

- *Frame* places the render's square in insert space: its center X and Z in meters, its turn about Y in degrees, its width in meters.
- *Elevations (deg)* are the four view elevations the channels of *Traced Light* were rendered from, off vertical.
- *Depths (mm)*: X is the cover's slab thickness, whose refraction the card undoes; Y is how deep the facets sit under the top; Z is how deep the glow's parallax reaches; W is how far in from the rim the traced light fades out.

*Alignment View* (in *Bulb*) shows the render's square on the insert as a 2 mm grid, red outside it and the traced light in green. Use it to set *Frame*.

## Look

- *Glow* is the traced glow's strength, in units of *Baked Scatter*: at one, it is as bright as the live glow would be.
- *Glow Spread* at one is the render as baked. Higher values lift the dim reaches toward the bright core, so the glow fills the insert instead of pooling over the bulb; lower values tighten it.
- *Live Glow Mix* is how much of the live soft and edge glow comes back in place of the traced one.
- *Glints* is the strength of the facets' highlights. A wide highlight is a dim one, so this sets how bright they are whatever their shape.
- *Glint Shape*: X is the facet tilt in degrees; Y is the angle sensitivity, how small a turn of the eye moves a highlight across a facet; Z is the number of highlights; W is the color fringe in degrees.
- *Glint Turn (deg)* turns the highlights about the insert.
- *Glint Organic*: X is how many degrees the molding's facets waver, so glints slide instead of switching whole facets; Y is how far each insert's highlights turn either way, seeded from where it sits, so a row of the same insert doesn't flash in step; Z is the waviness' wavelength in millimeters.

## Lamp Off

- *Lamp-Off Look* is how far the lamp-off render takes the place of the live unlit body: zero keeps the live body, one is the render as baked. The lamp's glow adds over it.
- *Lamp-Off Vary* lets each insert read the render a little differently, seeded from where it sits: X degrees of elevation, Y brightness as a fraction either way, Z a refraction wobble in texels. Zero is the render exactly, the same on every insert.
- *Lamp-Off Glints* is the room glinting in the facets with the lamp off. It is as strong as the room light the render shows there, with the glints' own shape and variation.

> [!NOTE]
> **The lamp-off brightness is a global.** `_InsertTracedOffEv`, in stops, scales every traced insert's lamp-off render to the table's room. Unset, it is 0, which shows the render at the brightness of the room it was rendered in. Set it when the table loads with `Shader.SetGlobalFloat("_InsertTracedOffEv", stops)`.

## Bulb

- *Through Traced Facets* at one draws the bulb through the traced facets, from the card; zero keeps the live material's own bulb image.
- *Gain* is the bulb's strength through the facets, on top of *Strength*.
- *Halo* is how much of the bulb's broad halo, the part that moves with the eye, comes through with its tight image.
- *Depth* scales how deep the bulbs sit for their image: more depth, more parallax.
- *Mesh Facet Normals* at one lights the body with the live mesh's facet normals. Zero gives it a flat polished top, as a real insert has, and leaves the facets to the traced maps.

> [!NOTE]
> **Shader properties.** The mode is the `_INSERT_TRACED` keyword. The controls use `_TracedLight`, `_TracedFacets`, `_TracedFrame`, `_TracedAngles`, `_TracedGeometry`, `_TracedGlow`, `_TracedGlowSpread`, `_TracedLiveMix`, `_TracedGlint`, `_TracedGlintParams`, `_TracedGlintRot`, `_TracedGlintOrganic`, `_TracedEnv`, `_TracedEnvGain`, `_TracedEnvVary`, `_TracedOffGlint`, `_TracedBulbSource`, `_TracedBulbGain`, `_TracedBulbHalo`, `_TracedBulbDepth`, `_TracedMeshNormals` and `_TracedDebug` (*Alignment View*). The shader code is `InsertTraced.hlsl`, next to the graph.
2 changes: 2 additions & 0 deletions VisualPinball.Unity/Documentation~/creators-guide/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
href: editor/inserts/presets.md
- name: Baking
href: editor/inserts/baking.md
- name: Traced Mode
href: editor/inserts/traced-mode.md
- name: Wire Rails
href: editor/wire-rails/index.md
items:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Visual Pinball Engine
// Copyright (C) 2026 freezy and VPE Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace VisualPinball.Unity.Editor
{
/// <summary>
/// The controls of the Insert Hybrid shader's opt-in Traced mode, in the
/// order the inspectors show them, with what each one does. HDRP and URP
/// draw them with their own controls; the words live here once.
/// </summary>
public static class InsertTracedFields
{
public const string Keyword = "_INSERT_TRACED";
public const string Title = "Traced Mode";

public static readonly GUIContent KeywordLabel = new("Traced Mode",
"Lights the body from a traced render instead of the optical response map. Off, the material is the live one, unchanged.");

public const string Note = "The body is shaded as a flat card at the top: the cover's refraction is undone, so nothing of the traced light moves with the mesh's facets. Colour, brightness and the lamp drive stay the Lamp section's.";

public readonly struct Entry
{
public readonly string Name;
public readonly GUIContent Label;
public readonly bool IsTexture;

public Entry(string name, string label, string tooltip, bool isTexture = false)
{
Name = name;
Label = new GUIContent(label, tooltip);
IsTexture = isTexture;
}
}

public readonly struct Section
{
public readonly string Title;
public readonly Entry[] Entries;

public Section(string title, params Entry[] entries)
{
Title = title;
Entries = entries;
}
}

public static readonly Section[] Sections = {
new("Bake",
new Entry("_TracedLight", "Traced Light", "The offline render of the lit insert: what the eye sees through each point of the top from four elevations, one per channel (Elevations below).", true),
new Entry("_TracedFacets", "Traced Facets", "The facet normals of the same render, X and Z in red and green, the faceted floor in alpha. The bulb and the glints refract through them.", true),
new Entry("_TracedFrame", "Frame", "Where the bake's square sits in insert space: its centre X and Z in metres, its turn about Y in degrees, its width in metres."),
new Entry("_TracedAngles", "Elevations (deg)", "The view elevations the four channels of the traced light were rendered from, off vertical."),
new Entry("_TracedGeometry", "Depths (mm)", "X: the cover's slab thickness, whose refraction the card undoes. Y: how deep the facets sit under the top. Z: how deep the glow's parallax reaches. W: how far in from the rim the traced light fades out.")),
new("Look",
new Entry("_TracedGlow", "Glow", "Strength of the traced glow, in units of Baked Scatter: one is as bright as the live glow would be."),
new Entry("_TracedGlowSpread", "Glow Spread", "One is the render as baked. More lifts the dim reaches toward the bright core, so the glow fills the insert instead of pooling over the bulb; less tightens it."),
new Entry("_TracedLiveMix", "Live Glow Mix", "How much of the live soft and edge glow comes back in place of the traced one."),
new Entry("_TracedGlint", "Glints", "Strength of the facets' highlights. A wide highlight is a dim one, so this sets how bright they are whatever their shape."),
new Entry("_TracedGlintParams", "Glint Shape", "X: facet tilt in degrees. Y: angle sensitivity, how small a turn of the eye moves a highlight across a facet (more is tighter and sharper). Z: number of highlights. W: colour fringe in degrees."),
new Entry("_TracedGlintRot", "Glint Turn (deg)", "Turns the highlights about the insert."),
new Entry("_TracedGlintOrganic", "Glint Organic", "X: how many degrees the moulding's facets waver, so glints slide instead of switching whole facets. Y: how far each insert's highlights turn either way, from where it sits, so a row of the same insert doesn't flash in step. Z: the waviness' wavelength in millimetres.")),
new("Lamp Off",
new Entry("_TracedEnv", "Lamp-Off Env", "The same render with the lamp off under the room: four elevations in a 2x2 atlas (15, 45 / 60, 70 degrees), multiplied by the body mask.", true),
new Entry("_TracedEnvGain", "Lamp-Off Look", "How far the traced lamp-off render takes the place of the live unlit body. Zero keeps the live body, one is the render as baked. The lamp's glow adds over it. Players dim or raise all of them together with the room brightness."),
new Entry("_TracedEnvVary", "Lamp-Off Vary", "Each insert reads the lamp-off render a little differently, from where it sits: X degrees of elevation, Y brightness (a fraction either way), Z a refraction wobble in texels. Zero is the render exactly, the same on every insert."),
new Entry("_TracedOffGlint", "Lamp-Off Glints", "The room glinting in the facets with the lamp off, as strong as the room light the render shows there, with the glints' own shape and per-insert variation.")),
new("Bulb",
new Entry("_TracedBulbSource", "Through Traced Facets", "One draws the bulb through the traced facets, from the card; zero keeps the live material's own image."),
new Entry("_TracedBulbGain", "Gain", "Strength of the bulb seen through the facets, on top of Lamp Strength."),
new Entry("_TracedBulbHalo", "Halo", "How much of the bulb's broad halo, the part that moves with the eye, comes through with its tight image."),
new Entry("_TracedBulbDepth", "Depth", "Scales how deep the bulbs sit for their image: more depth, more parallax."),
new Entry("_TracedMeshNormals", "Mesh Facet Normals", "One lights the body with the live mesh's facet normals; zero gives it a flat polished top, as a real insert has, and leaves the facets to the traced maps."),
new Entry("_TracedDebug", "Alignment View", "Shows the bake's square on the insert: a 2 mm grid, red outside it, the traced light in green.")),
};

private static readonly HashSet<string> Names = new(Sections.SelectMany(section => section.Entries).Select(entry => entry.Name).Append(Keyword));

/// <summary>Whether a property is one of the Traced controls, so a generic list can leave it to them.</summary>
public static bool Owns(string property) => Names.Contains(property);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public void ShouldApplyBodySettingsWithoutChangingColorsMapsOrCover()
source.SetVector("_OpticalLamp", new Vector4(0.01f, -0.02f, 0.03f, 0.009f));
source.SetVector("_OpticalStarburst", new Vector4(1f, 2f, 3f, 4f));
source.SetFloat("_InsertLampLevel", 0.7f);
source.SetFloat("_INSERT_TRACED", 1f);
source.SetFloat("_TracedGlow", 0.5f);
source.SetVector("_TracedFrame", new Vector4(0.001f, 0.002f, 90f, 0.034f));
source.SetTexture("_NormalMap", sourceMap);
sourceCover.SetColor("_HazeColor", new Color(0.8f, 0.8f, 0.8f));
sourceCover.SetFloat("_CoverOpacity", 0.365f);
Expand All @@ -57,6 +60,7 @@ public void ShouldApplyBodySettingsWithoutChangingColorsMapsOrCover()
Assert.That(preset.Body, Has.None.Matches<InsertMaterialPreset.Entry>(e => e.Name == "_NormalMap"), "new presets exclude texture maps");
Assert.That(preset.Body, Has.None.Matches<InsertMaterialPreset.Entry>(e => e.Name is "_OpticalLamp" or "_OpticalLampB" or "_OpticalLampC" or "_OpticalLampD"), "the virtual lamps are the insert's own");
Assert.That(preset.Body, Has.None.Matches<InsertMaterialPreset.Entry>(e => e.Name == "_OpticalStarburst" || e.Name == "_InsertLampLevel"), "hidden bake and runtime values stay out");
Assert.That(preset.Body, Has.None.Matches<InsertMaterialPreset.Entry>(e => e.Name == "_INSERT_TRACED" || e.Name.StartsWith("_Traced")), "the Traced mode and its bake's values are the insert's own");
Assert.That(preset.Cover, Is.Empty, "new presets exclude the cover material");
Assert.That(preset.CoverShader, Is.Empty);

Expand All @@ -75,6 +79,7 @@ public void ShouldApplyBodySettingsWithoutChangingColorsMapsOrCover()
target.SetColor("_EmissiveColor", Color.magenta);
target.SetVector("_OpticalLamp", new Vector4(0f, -0.015f, 0f, 0.007f));
target.SetVector("_OpticalStarburst", new Vector4(9f, 9f, 9f, 9f));
target.SetVector("_TracedFrame", new Vector4(0f, 0f, 0f, 0.05f));
target.SetTexture("_NormalMap", targetMap);
target.SetTextureScale("_NormalMap", new Vector2(2f, 3f));
targetCover.SetColor("_HazeColor", Color.red);
Expand All @@ -91,6 +96,7 @@ public void ShouldApplyBodySettingsWithoutChangingColorsMapsOrCover()
Assert.That(target.GetColor("_EmissiveColor"), Is.EqualTo(Color.magenta));
Assert.That(target.GetVector("_OpticalLamp"), Is.EqualTo(new Vector4(0f, -0.015f, 0f, 0.007f)), "the target keeps its own lamp");
Assert.That(target.GetVector("_OpticalStarburst"), Is.EqualTo(new Vector4(9f, 9f, 9f, 9f)), "and its own bake");
Assert.That(target.GetVector("_TracedFrame"), Is.EqualTo(new Vector4(0f, 0f, 0f, 0.05f)), "and its own traced bake");
Assert.That(target.GetTexture("_NormalMap"), Is.SameAs(targetMap), "and its own maps");
Assert.That(target.GetTextureScale("_NormalMap"), Is.EqualTo(new Vector2(2f, 3f)));
Assert.That(targetCover.GetColor("_HazeColor"), Is.EqualTo(Color.red));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ public sealed class Entry
/// </summary>
private static readonly HashSet<string> Excluded = new() { "_OpticalLamp", "_OpticalLampB", "_OpticalLampC", "_OpticalLampD", "_InsertLuminosity" };

/// <summary>
/// The opt-in Traced mode and its values are the insert type's and its
/// bake's (where the render sits, its elevations, the look tuned to it),
/// not a look to share between inserts.
/// </summary>
private static bool IsTraced(string name) => name == "_INSERT_TRACED" || name.StartsWith("_Traced", StringComparison.Ordinal);

/// <summary>The preset's value for the property, or null when it has none.</summary>
public Entry Find(string name) => Body?.Find(entry => entry != null && entry.Name == name && entry.Kind != Kind.Color && !Excluded.Contains(name));

Expand Down Expand Up @@ -121,7 +128,8 @@ public static bool IsStyle(Shader shader, int index)
if ((shader.GetPropertyFlags(index) & ShaderPropertyFlags.HideInInspector) != 0) {
return false;
}
return !Excluded.Contains(shader.GetPropertyName(index));
var name = shader.GetPropertyName(index);
return !Excluded.Contains(name) && !IsTraced(name);
}

private static List<Entry> Read(Material material)
Expand Down