summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2026-01-13 19:33:51 -0800
committeryum <yum.food.vr@gmail.com>2026-01-13 19:33:51 -0800
commit8c3a05445f529c10ebbf5bfdc0eb220fe95c558c (patch)
treecd15a6589e819f9a75de214335cda2bdf7492cab
parentb0982529d9e3d549106edd80a3e1246f3fb8cd2c (diff)
Fold: add hemi octahedron to plane operator
-rw-r--r--Scripts/Fold/Editor/FoldEditorWindow.cs564
-rw-r--r--Scripts/Fold/Editor/FoldPipelineBuilder.cs14
-rw-r--r--vertex.cginc23
-rw-r--r--vertex_deformation.slang58
4 files changed, 659 insertions, 0 deletions
diff --git a/Scripts/Fold/Editor/FoldEditorWindow.cs b/Scripts/Fold/Editor/FoldEditorWindow.cs
new file mode 100644
index 0000000..ab02781
--- /dev/null
+++ b/Scripts/Fold/Editor/FoldEditorWindow.cs
@@ -0,0 +1,564 @@
+using System;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+
+public class FoldEditorWindow : EditorWindow
+{
+ [SerializeField] Material targetMaterial;
+ [SerializeField] Vector2 scrollPos;
+ [SerializeReference] List<DeformOperation> operations = new();
+ [SerializeField] List<int> expandedOps = new();
+ [SerializeField] bool liveUpdate = true;
+
+ [MenuItem("Tools/yum_food/Fold")]
+ static void ShowWindow()
+ {
+ var window = GetWindow<FoldEditorWindow>("Fold");
+ window.minSize = new Vector2(400, 300);
+ }
+
+ void OnEnable()
+ {
+ // Operations list is restored via SerializeReference
+ }
+
+ void OnGUI()
+ {
+ EditorGUILayout.Space(5);
+
+ DrawHeader();
+ EditorGUILayout.Space(10);
+
+ if (targetMaterial == null)
+ {
+ EditorGUILayout.HelpBox("Select a material to build deformation pipelines", MessageType.Info);
+ return;
+ }
+
+ DrawToolbar();
+ EditorGUILayout.Space(5);
+
+ EditorGUI.BeginChangeCheck();
+
+ scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
+ DrawOperationsList();
+ EditorGUILayout.EndScrollView();
+
+ if (EditorGUI.EndChangeCheck() && liveUpdate && targetMaterial != null)
+ ApplyToMaterial();
+
+ EditorGUILayout.Space(5);
+ DrawFooter();
+ }
+
+ void DrawHeader()
+ {
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField("Target Material", EditorStyles.boldLabel, GUILayout.Width(100));
+
+ var newMat = EditorGUILayout.ObjectField(targetMaterial, typeof(Material), false) as Material;
+ if (newMat != targetMaterial)
+ {
+ targetMaterial = newMat;
+ if (targetMaterial != null)
+ LoadFromMaterial();
+ }
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ void DrawToolbar()
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+
+ bool pipelineFull = operations.Count >= 16;
+ using (new EditorGUI.DisabledScope(pipelineFull))
+ {
+ var content = pipelineFull
+ ? new GUIContent("Add Operation", "Pipeline full (16/16)")
+ : new GUIContent("Add Operation");
+ if (GUILayout.Button(content, EditorStyles.toolbarDropDown, GUILayout.Width(120)))
+ ShowAddOperationMenu();
+ }
+
+ GUILayout.FlexibleSpace();
+
+ liveUpdate = GUILayout.Toggle(liveUpdate, "Live", EditorStyles.toolbarButton, GUILayout.Width(40));
+
+ if (GUILayout.Button("Load Presets", EditorStyles.toolbarDropDown, GUILayout.Width(100)))
+ ShowPresetsMenu();
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ void DrawOperationsList()
+ {
+ if (operations.Count == 0)
+ {
+ EditorGUILayout.HelpBox("No operations. Click 'Add Operation' to begin.", MessageType.Info);
+ return;
+ }
+
+ for (int i = 0; i < operations.Count; i++)
+ {
+ DrawOperation(i);
+ EditorGUILayout.Space(3);
+ }
+ }
+
+ void DrawOperation(int index)
+ {
+ var op = operations[index];
+ bool isExpanded = expandedOps.Contains(index);
+
+ var bgColor = isExpanded ? new Color(0.3f, 0.5f, 0.8f, 0.3f) : new Color(0.2f, 0.2f, 0.2f, 0.3f);
+
+ EditorGUILayout.BeginVertical(GUI.skin.box);
+ GUI.backgroundColor = bgColor;
+
+ // Header with drag handle and delete button
+ EditorGUILayout.BeginHorizontal();
+
+ // Drag handle
+ GUILayout.Label($"#{index}", GUILayout.Width(30));
+
+ // Operation name (clickable to expand/collapse)
+ if (GUILayout.Button(op.GetDisplayName(), EditorStyles.boldLabel))
+ {
+ if (isExpanded) expandedOps.Remove(index);
+ else expandedOps.Add(index);
+ }
+
+ GUILayout.FlexibleSpace();
+
+ // Move up/down
+ GUI.enabled = index > 0;
+ if (GUILayout.Button("▲", GUILayout.Width(25)))
+ {
+ operations.RemoveAt(index);
+ operations.Insert(index - 1, op);
+ if (expandedOps.Contains(index))
+ {
+ expandedOps.Remove(index);
+ expandedOps.Add(index - 1);
+ }
+ }
+
+ GUI.enabled = index < operations.Count - 1;
+ if (GUILayout.Button("▼", GUILayout.Width(25)))
+ {
+ operations.RemoveAt(index);
+ operations.Insert(index + 1, op);
+ if (expandedOps.Contains(index))
+ {
+ expandedOps.Remove(index);
+ expandedOps.Add(index + 1);
+ }
+ }
+
+ GUI.enabled = true;
+
+ // Delete button
+ if (GUILayout.Button("×", GUILayout.Width(25)))
+ {
+ operations.RemoveAt(index);
+ expandedOps.Remove(index);
+ // Adjust indices in expandedOps
+ for (int i = 0; i < expandedOps.Count; i++)
+ {
+ if (expandedOps[i] > index) expandedOps[i]--;
+ }
+ }
+
+ EditorGUILayout.EndHorizontal();
+
+ // Draw parameters when expanded
+ if (isExpanded)
+ {
+ EditorGUILayout.Space(5);
+ EditorGUI.indentLevel++;
+ op.DrawParameters();
+ EditorGUI.indentLevel--;
+ }
+
+ GUI.backgroundColor = Color.white;
+ EditorGUILayout.EndVertical();
+ }
+
+ void DrawFooter()
+ {
+ EditorGUILayout.BeginHorizontal();
+
+ if (GUILayout.Button("Apply to Material", GUILayout.Height(30)))
+ ApplyToMaterial();
+
+ if (GUILayout.Button("Clear All", GUILayout.Height(30), GUILayout.Width(100)))
+ {
+ operations.Clear();
+ expandedOps.Clear();
+ }
+
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(5);
+ EditorGUILayout.LabelField($"Operations: {operations.Count}/16", EditorStyles.miniLabel);
+ }
+
+ void ShowAddOperationMenu()
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("Tube to Plane"), false, () => AddOperation(new TubeToPlaneOp()));
+ menu.AddItem(new GUIContent("Plane to Tube"), false, () => AddOperation(new PlaneToTubeOp()));
+ menu.AddItem(new GUIContent("Plane to Hemi-Octahedron"), false, () => AddOperation(new PlaneToHemiOctahedronOp()));
+ menu.AddItem(new GUIContent("Hemi-Octahedron to Plane"), false, () => AddOperation(new HemiOctahedronToPlaneOp()));
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent("Point Align"), false, () => AddOperation(new PointAlignOp()));
+ menu.AddItem(new GUIContent("Axis Align"), false, () => AddOperation(new AxisAlignOp()));
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent("Norm Conversion"), false, () => AddOperation(new NormConversionOp()));
+ menu.AddItem(new GUIContent("Seal"), false, () => AddOperation(new SealOp()));
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent("Sine Waves"), false, () => AddOperation(new SineWavesOp()));
+ menu.AddItem(new GUIContent("FBM"), false, () => AddOperation(new FBMOp()));
+
+ menu.ShowAsContext();
+ }
+
+ void ShowPresetsMenu()
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("Tube to Plane"), false, () => LoadPreset_TubeToPlane());
+ menu.AddItem(new GUIContent("Plane to Tube"), false, () => LoadPreset_PlaneToTube());
+ menu.AddItem(new GUIContent("Plane to Hemi-Octahedron"), false, () => LoadPreset_PlaneToHemiOct());
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent("Norm: L1→L2 (Diamond→Sphere)"), false, () => LoadPreset_NormL1L2());
+ menu.AddItem(new GUIContent("Norm: L2→L1 (Sphere→Diamond)"), false, () => LoadPreset_NormL2L1());
+ menu.AddItem(new GUIContent("Norm: L2→L∞ (Sphere→Cube)"), false, () => LoadPreset_NormL2Linf());
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent("Complex/Tube→Plane + Norm Conv"), false, () => LoadPreset_TubePlaneNorm());
+
+ menu.ShowAsContext();
+ }
+
+ void AddOperation(DeformOperation op)
+ {
+ if (operations.Count >= 16)
+ return;
+ operations.Add(op);
+ expandedOps.Add(operations.Count - 1);
+ }
+
+ void LoadPreset_TubeToPlane()
+ {
+ operations.Clear();
+ expandedOps.Clear();
+ AddOperation(new TubeToPlaneOp());
+ }
+
+ void LoadPreset_PlaneToTube()
+ {
+ operations.Clear();
+ expandedOps.Clear();
+ AddOperation(new PlaneToTubeOp());
+ }
+
+ void LoadPreset_PlaneToHemiOct()
+ {
+ operations.Clear();
+ expandedOps.Clear();
+ AddOperation(new PlaneToHemiOctahedronOp());
+ }
+
+ void LoadPreset_NormL1L2()
+ {
+ operations.Clear();
+ expandedOps.Clear();
+ var op = new NormConversionOp();
+ op.inputK = 1f;
+ op.outputK = 2f;
+ AddOperation(op);
+ }
+
+ void LoadPreset_NormL2L1()
+ {
+ operations.Clear();
+ expandedOps.Clear();
+ var op = new NormConversionOp();
+ op.inputK = 2f;
+ op.outputK = 1f;
+ AddOperation(op);
+ }
+
+ void LoadPreset_NormL2Linf()
+ {
+ operations.Clear();
+ expandedOps.Clear();
+ var op = new NormConversionOp();
+ op.inputK = 2f;
+ op.outputK = float.PositiveInfinity;
+ AddOperation(op);
+ }
+
+ void LoadPreset_TubePlaneNorm()
+ {
+ operations.Clear();
+ expandedOps.Clear();
+ AddOperation(new TubeToPlaneOp());
+ var norm = new NormConversionOp();
+ norm.inputK = 2f;
+ norm.outputK = 1f;
+ AddOperation(norm);
+ }
+
+ void ApplyToMaterial()
+ {
+ var builder = FoldPipelineBuilder.Create().For(targetMaterial);
+
+ foreach (var op in operations)
+ op.ApplyTo(builder);
+
+ builder.Apply();
+ }
+
+ void LoadFromMaterial()
+ {
+ // Could potentially read back from material properties here
+ // For now, just clear the list
+ operations.Clear();
+ expandedOps.Clear();
+ }
+}
+
+// Base class for deformation operations
+[System.Serializable]
+public abstract class DeformOperation
+{
+ public abstract string GetDisplayName();
+ public abstract void DrawParameters();
+ public abstract void ApplyTo(FoldPipelineBuilder builder);
+}
+
+[System.Serializable]
+public class TubeToPlaneOp : DeformOperation
+{
+ public Vector3 p = Vector3.zero;
+ public Vector3 r = Vector3.right;
+ public Vector3 s = Vector3.forward;
+ public float t = 1f;
+
+ public override string GetDisplayName() => "Tube to Plane";
+
+ public override void DrawParameters()
+ {
+ p = EditorGUILayout.Vector3Field("Origin (p)", p);
+ r = EditorGUILayout.Vector3Field("R Axis", r);
+ s = EditorGUILayout.Vector3Field("S Axis", s);
+ t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.TubeToPlane(p, r, s, t);
+}
+
+[System.Serializable]
+public class PlaneToTubeOp : DeformOperation
+{
+ public Vector3 p = Vector3.zero;
+ public Vector3 r = Vector3.right;
+ public Vector3 s = Vector3.forward;
+ public float t = 1f;
+
+ public override string GetDisplayName() => "Plane to Tube";
+
+ public override void DrawParameters()
+ {
+ p = EditorGUILayout.Vector3Field("Origin (p)", p);
+ r = EditorGUILayout.Vector3Field("R Axis", r);
+ s = EditorGUILayout.Vector3Field("S Axis", s);
+ t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.PlaneToTube(p, r, s, t);
+}
+
+[System.Serializable]
+public class PlaneToHemiOctahedronOp : DeformOperation
+{
+ public Vector3 p = Vector3.zero;
+ public Vector3 r = Vector3.right;
+ public Vector3 s = Vector3.forward;
+ public float t = 1f;
+
+ public override string GetDisplayName() => "Plane to Hemi-Octahedron";
+
+ public override void DrawParameters()
+ {
+ p = EditorGUILayout.Vector3Field("Origin (p)", p);
+ r = EditorGUILayout.Vector3Field("R Axis", r);
+ s = EditorGUILayout.Vector3Field("S Axis", s);
+ t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.PlaneToHemiOctahedron(p, r, s, t);
+}
+
+[System.Serializable]
+public class HemiOctahedronToPlaneOp : DeformOperation
+{
+ public Vector3 p = Vector3.zero;
+ public Vector3 r = Vector3.right;
+ public Vector3 s = Vector3.forward;
+ public float t = 1f;
+
+ public override string GetDisplayName() => "Hemi-Octahedron to Plane";
+
+ public override void DrawParameters()
+ {
+ p = EditorGUILayout.Vector3Field("Origin (p)", p);
+ r = EditorGUILayout.Vector3Field("R Axis", r);
+ s = EditorGUILayout.Vector3Field("S Axis", s);
+ t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.HemiOctahedronToPlane(p, r, s, t);
+}
+
+[System.Serializable]
+public class PointAlignOp : DeformOperation
+{
+ public Vector3 po = Vector3.zero;
+ public Vector3 pp = Vector3.up;
+ public Vector3 r = Vector3.right;
+ public float t = 1f;
+
+ public override string GetDisplayName() => "Point Align";
+
+ public override void DrawParameters()
+ {
+ po = EditorGUILayout.Vector3Field("Origin Point (po)", po);
+ pp = EditorGUILayout.Vector3Field("Target Point (pp)", pp);
+ r = EditorGUILayout.Vector3Field("Rotation Axis (r)", r);
+ t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.PointAlign(po, pp, r, t);
+}
+
+[System.Serializable]
+public class AxisAlignOp : DeformOperation
+{
+ public Vector3 po = Vector3.zero;
+ public Vector3 pp = Vector3.up;
+ public Vector3 r = Vector3.right;
+ public float t = 1f;
+
+ public override string GetDisplayName() => "Axis Align";
+
+ public override void DrawParameters()
+ {
+ po = EditorGUILayout.Vector3Field("Origin Point (po)", po);
+ pp = EditorGUILayout.Vector3Field("Target Point (pp)", pp);
+ r = EditorGUILayout.Vector3Field("Rotation Axis (r)", r);
+ t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.AxisAlign(po, pp, r, t);
+}
+
+[System.Serializable]
+public class NormConversionOp : DeformOperation
+{
+ public float inputK = 2f;
+ public float outputK = 1f;
+ public float t = 1f;
+
+ public override string GetDisplayName() => $"Norm Conversion (L{FormatNorm(inputK)}→L{FormatNorm(outputK)})";
+
+ string FormatNorm(float k)
+ {
+ if (float.IsPositiveInfinity(k)) return "∞";
+ return k.ToString("F1");
+ }
+
+ public override void DrawParameters()
+ {
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField("Input Norm (k)", GUILayout.Width(120));
+ inputK = EditorGUILayout.FloatField(inputK);
+ if (GUILayout.Button("L∞", GUILayout.Width(30))) inputK = float.PositiveInfinity;
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField("Output Norm (k)", GUILayout.Width(120));
+ outputK = EditorGUILayout.FloatField(outputK);
+ if (GUILayout.Button("L∞", GUILayout.Width(30))) outputK = float.PositiveInfinity;
+ EditorGUILayout.EndHorizontal();
+
+ t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f);
+
+ EditorGUILayout.HelpBox(
+ "Common: L1=diamond, L2=sphere, L∞=cube",
+ MessageType.Info);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.NormConversion(inputK, outputK, t);
+}
+
+[System.Serializable]
+public class SealOp : DeformOperation
+{
+ public float A = 0.1f;
+ public float k = 2f;
+ public float st = 0.8f;
+ public float t = 1f;
+
+ public override string GetDisplayName() => "Seal";
+
+ public override void DrawParameters()
+ {
+ A = EditorGUILayout.FloatField("Amplitude (A)", A);
+ k = EditorGUILayout.FloatField("Smoothness (k)", k);
+ st = EditorGUILayout.FloatField("Scale (st)", st);
+ t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.Seal(A, k, st, t);
+}
+
+[System.Serializable]
+public class SineWavesOp : DeformOperation
+{
+ public override string GetDisplayName() => "Sine Waves";
+
+ public override void DrawParameters()
+ {
+ EditorGUILayout.HelpBox("Sine waves use shader-side parameters", MessageType.Info);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.SineWaves();
+}
+
+[System.Serializable]
+public class FBMOp : DeformOperation
+{
+ public override string GetDisplayName() => "FBM (Fractal Brownian Motion)";
+
+ public override void DrawParameters()
+ {
+ EditorGUILayout.HelpBox("FBM uses shader-side parameters", MessageType.Info);
+ }
+
+ public override void ApplyTo(FoldPipelineBuilder builder) =>
+ builder.FBM();
+}
diff --git a/Scripts/Fold/Editor/FoldPipelineBuilder.cs b/Scripts/Fold/Editor/FoldPipelineBuilder.cs
index 64032a5..3731cf8 100644
--- a/Scripts/Fold/Editor/FoldPipelineBuilder.cs
+++ b/Scripts/Fold/Editor/FoldPipelineBuilder.cs
@@ -57,6 +57,7 @@ public class FoldPipelineBuilder
public const int SineWaves = 7;
public const int FBM = 8;
public const int PlaneToHemiOctahedron = 9;
+ public const int HemiOctahedronToPlane = 10;
}
FoldPipelineBuilder() { }
@@ -108,6 +109,19 @@ public class FoldPipelineBuilder
return this;
}
+ public FoldPipelineBuilder HemiOctahedronToPlane(Vector3 p, Vector3 r, Vector3 s, float t)
+ {
+ slots.Add(new FoldSlot
+ {
+ opcode = Opcodes.HemiOctahedronToPlane,
+ vec0 = p,
+ vec1 = r,
+ vec2 = s,
+ float0 = t
+ });
+ return this;
+ }
+
public FoldPipelineBuilder PointAlign(Vector3 po, Vector3 pp, Vector3 r, float t)
{
slots.Add(new FoldSlot
diff --git a/vertex.cginc b/vertex.cginc
index e59813c..77412a9 100644
--- a/vertex.cginc
+++ b/vertex.cginc
@@ -14,6 +14,7 @@
#define OPCODE_SINE_WAVES 7
#define OPCODE_FBM 8
#define OPCODE_PLANE_TO_HEMI_OCTAHEDRON 9
+#define OPCODE_HEMI_OCTAHEDRON_TO_PLANE 10
#if defined(_VERTEX_DEFORMATION)
@@ -165,6 +166,22 @@ void apply_plane_to_hemi_octahedron_normal(inout float3 objPos, inout float3 obj
plane_to_hemi_octahedron_normal(objPos, objNorm, objTan, p, r, s, t);
}
+void apply_hemi_octahedron_to_plane(inout float3 objPos, float4 v0, float4 v1, float4 v2, float f0) {
+ float3 p = v0.xyz;
+ float3 r = v1.xyz;
+ float3 s = v2.xyz;
+ float t = f0;
+ objPos = hemi_octahedron_to_plane(objPos, p, r, s, t);
+}
+
+void apply_hemi_octahedron_to_plane_normal(inout float3 objPos, inout float3 objNorm, inout float3 objTan, float4 v0, float4 v1, float4 v2, float f0) {
+ float3 p = v0.xyz;
+ float3 r = v1.xyz;
+ float3 s = v2.xyz;
+ float t = f0;
+ hemi_octahedron_to_plane_normal(objPos, objNorm, objTan, p, r, s, t);
+}
+
void deform(inout float3 objPos) {
const float t = getTime();
@@ -400,6 +417,9 @@ void deform(inout float3 objPos) {
case OPCODE_PLANE_TO_HEMI_OCTAHEDRON:
apply_plane_to_hemi_octahedron(objPos, v0, v1, v2, f0);
break;
+ case OPCODE_HEMI_OCTAHEDRON_TO_PLANE:
+ apply_hemi_octahedron_to_plane(objPos, v0, v1, v2, f0);
+ break;
}
}
}
@@ -642,6 +662,9 @@ void deform_normal(inout float3 objPos, inout float3 objNorm, inout float3 objTa
case OPCODE_PLANE_TO_HEMI_OCTAHEDRON:
apply_plane_to_hemi_octahedron_normal(objPos, objNorm, objTan, v0, v1, v2, f0);
break;
+ case OPCODE_HEMI_OCTAHEDRON_TO_PLANE:
+ apply_hemi_octahedron_to_plane_normal(objPos, objNorm, objTan, v0, v1, v2, f0);
+ break;
}
}
}
diff --git a/vertex_deformation.slang b/vertex_deformation.slang
index 50c252e..6e916ed 100644
--- a/vertex_deformation.slang
+++ b/vertex_deformation.slang
@@ -625,4 +625,62 @@ public void plane_to_hemi_octahedron_undeform_normal(float3 xyz, inout float3 no
R3R3_UNDEFORM_NORMAL_AND_TANGENT(normal, tangent);
}
+// Maps a hemi-octahedron to a plane (inverse of plane_to_hemi_octahedron).
+// Input: unit hemisphere with pole at +s direction
+// Output: plane on [-1,1]² in the (r, rxs) plane
+[Differentiable]
+public float3 hemi_octahedron_to_plane(float3 xyz,
+ no_diff float3 p, no_diff float3 r_cart, no_diff float3 s_cart,
+ no_diff float t) {
+ // Convert from cartesian to (r, s, r x s) space.
+ r_cart = normalize(r_cart);
+ s_cart = normalize(s_cart);
+ float3 rxs_cart = cross(s_cart, r_cart);
+ float3x3 to_rsrxs = float3x3(r_cart, s_cart, rxs_cart);
+ float3x3 to_cart = transpose(to_rsrxs);
+
+ // Translate origin to `p` then change into (r, s, r x s) basis.
+ xyz = mul(to_rsrxs, xyz - p);
+
+ float3 xyz0 = xyz;
+
+ // Undo the -45° unrotation from forward pass (rotate by +45°)
+ float x_rot = (xyz.x + xyz.z) * RCP_SQRT_2;
+ float z_rot = (xyz.z - xyz.x) * RCP_SQRT_2;
+
+ // Octahedral encode: project normalized direction to plane
+ float3 N = normalize(float3(x_rot, xyz.y, z_rot));
+ N.y = dmax(N.y, 1e-4f);
+ float L1 = dabs(N.x) + dabs(N.y) + dabs(N.z);
+ float x_oct = N.x / L1;
+ float z_oct = N.z / L1;
+
+ // Undo the initial 45° rotation (x_rot = (x+z)*0.5, z_rot = (z-x)*0.5)
+ // Inverse: x = x_rot - z_rot, z = x_rot + z_rot
+ float x_plane = x_oct - z_oct;
+ float z_plane = x_oct + z_oct;
+
+ float3 plane_pos = float3(x_plane, 0.0f, z_plane);
+
+ // Interpolate between original position and plane position
+ float3 result = dlerp(xyz0, plane_pos, dmin(t, 1.0f));
+
+ // Map back to cartesian basis
+ xyz = mul(to_cart, result) + p;
+
+ return xyz;
+}
+
+public void hemi_octahedron_to_plane_normal(inout float3 xyz, inout float3 normal,
+ inout float3 tangent, float3 p, float3 r, float3 s, float t) {
+ R3R3_NORMALS(xyz, normal, tangent, hemi_octahedron_to_plane, p, r, s, t);
+}
+
+public void hemi_octahedron_to_plane_undeform_normal(float3 xyz, inout float3 normal,
+ inout float3 tangent, float3 p, float3 r, float3 s, float t) {
+ R3R3_DECLARE_BASIS_VECTORS(xyz);
+ R3R3_AUTODIFF_BASIS_VECTORS(hemi_octahedron_to_plane, p, r, s, t);
+ R3R3_UNDEFORM_NORMAL_AND_TANGENT(normal, tangent);
+}
+
#endif // __CUSTOM31_INC