Creating Custom SDF Modifier
Raymarcher includes a library of practical modifiers, including various deformations, displacements, target boolean operations, and more.
You have the possibility to create your own SDF modifier with custom properties and formulas.
To do so, create a new C# script and inherit from
RMObjectModifierBase class within the Raymarcher.Objects.Modifiers namespace, defining the actual Raymarcher SDF object modifier.
Please be aware that modifiers can store properties of the following datatypes:
float, float2, float3, float4, sampler2D, sampler3D, and float4x4.It's important to note that modifiers are not allowed to use
keywords or shader features.
Example SDF Fragment Modifier
Take a look at the example reconstructed script for sdf fragment modifier:
using UnityEngine;
using Raymarcher.Objects.Modifiers;
public sealed class SdfFragmentModifier : RMObjectModifierBase
{
public Vector3 fragmentTiling = Vector3.one * 5;
public float fragmentEvolution = 0.1f;
public Vector3 fragmentScroll = Vector3.zero;
private const string FRAGMENT_TILING = nameof(fragmentTiling);
private const string FRAGMENT_SCROLL = nameof(fragmentScroll);
public override string SdfMethodBody =>
$@"float frg = (sin({FRAGMENT_TILING}.x * ({VARCONST_POSITION}.x + _Time.y * {FRAGMENT_SCROLL}.x))
* sin({FRAGMENT_TILING}.y * ({VARCONST_POSITION}.y + _Time.y * {FRAGMENT_SCROLL}.y))
* sin({FRAGMENT_TILING}.z * ({VARCONST_POSITION}.z + _Time.y * {FRAGMENT_SCROLL}.z))) * {FRAGMENT_TILING}.w;
{VARCONST_SDF} += frg;";
public override string SdfMethodName => "FragmentModifier";
public override ISDFEntity.SDFUniformField[] SdfUniformFields =>
new ISDFEntity.SDFUniformField[2]
{
new ISDFEntity.SDFUniformField(FRAGMENT_TILING, ISDFEntity.SDFUniformType.Float4),
new ISDFEntity.SDFUniformField(FRAGMENT_SCROLL, ISDFEntity.SDFUniformType.Float3)
};
public override void PushSdfEntityToShader(in Material raymarcherSessionMaterial, in string iterationIndex)
{
raymarcherSessionMaterial.SetVector(FRAGMENT_TILING + iterationIndex,
new Vector4(fragmentTiling.x, fragmentTiling.y, fragmentTiling.z, fragmentEvolution));
raymarcherSessionMaterial.SetVector(FRAGMENT_SCROLL + iterationIndex, fragmentScroll);
}
}
The example script demonstrates the creation of the fragment/sphere-displacement modifier.
Once this object inherits from RMObjectModifierBase, the SDF object will automatically detect this modifier upon the modifier's creation.
As the object is added to Raymarcher's Object Buffer, it becomes necessary to recompile the Object Buffer to synchronize the session shader with the current data.
Further steps for this process will be displayed in the editor.
Built-in Raymarcher Parameters & Compiled Format
Raymarcher compiler defines SDF objects and modifiers in a specific format.
Modifiers provide the capability to modify already created SDF objects in three different 'inline modes':
1. Sdf Instance Position
In this mode, your modifier's input parameters include the currently marched ray position only, along with the modifier's properties. This is suitable for modifying the marched position only.
Sdf Instance Position inline mode syntax:
2. Post Sdf Instance
In this mode, your modifier's input parameters include five built-in parameters (if the render type is either set to Quality or Standard), along with the modifier's properties.
The five built-in parameters are: Currently calculated SDF value (sdf), marched position (p), currently calculated color (color), material type (materialType), and material instance index (materialInstance).
This is useful for modifying the entire SDF object after its direct initialization.
3. Post Sdf Buffer
In this mode, your modifier's input parameters include four built-in parameters (if the render type is either set to Quality or Standard), along with the modifier's properties.
The four built-in parameters are: Currently calculated SDF value (sdf), currently calculated color (color), material type (materialType), and material instance index (materialInstance).
This mode is useful for creating target operations with already calculated SDF objects, allowing direct interaction between them.
Post Sdf Instance and Post Sdf Buffer inline modes syntax:
As seen in the SdfMethodBody example above, certain constants are used, such as 'VARCONST_SDF' or 'VARCONST_POSITION'.
These constants represent Raymarcher's compiler-defined macros included in every modifier method's parameters.
Use the VARCONST_POSITION to retrieve the currently raymarched position.
Use the VARCONST_COLOR to modify or retrieve the current object's color.
Use the VARCONST_SDF to define the actual SDF formula.
Use the VARCONST_MATERIAL_INSTANCE to modify or retrieve the current object's material instance index.
Use the VARCONST_MATERIAL_TYPE to modify or retrieve the current object's material type index.
Thus, the example code provided above would be converted to the shader code like this:
(the value precisions are automatized, as each Render Type uses different value precision; the following generated code works for the Standard render type only)
float4 FragmentModifier(float sdf, float3 p, half color, half materialInstance, half materialType, half4 fragmentTiling, half3 fragmentScroll)
{
float frg = (sin(fragmentTiling.x * (p.x + _Time.y * fragmentScroll.x))
* sin(fragmentTiling.y * (p.y + _Time.y * fragmentScroll.y))
* sin(fragmentTiling.z * (p.z + _Time.y * fragmentScroll.z))) * fragmentTiling.w;
sdf += frg;
return float4(sdf, color, materialInstance, materialType);
}
Modifier Shared Containers
The shared modifier container prevents the creation of multiple modifier instances.
It caches just one container instance and utilizes it across its owners.
This option is particularly useful when your SDF modifier is complex and used on multiple SDF objects.
The shared modifier allows you to share the same data among multiple SDF objects.
Consider the extended code for the fragment modifier with support for shared modifiers:
using UnityEngine;
using Raymarcher.Objects.Modifiers;
public sealed class SdfFragmentModifier : RMObjectModifierBase
{
public FragmentSharedData fragmentSharedData;
[System.Serializable]
public sealed class FragmentSharedData
{
public Vector3 fragmentTiling = Vector3.one * 5;
public float fragmentEvolution = 0.1f;
[Space]
public Vector3 fragmentScroll = Vector3.zero;
}
private const string FRAGMENT_TILING = nameof(FragmentSharedData.fragmentTiling);
private const string FRAGMENT_SCROLL = nameof(FragmentSharedData.fragmentScroll);
public override string SdfMethodBody =>
$@"float frg = (sin({FRAGMENT_TILING}.x * ({VARCONST_POSITION}.x + _Time.y * {FRAGMENT_SCROLL}.x))
* sin({FRAGMENT_TILING}.y * ({VARCONST_POSITION}.y + _Time.y * {FRAGMENT_SCROLL}.y))
* sin({FRAGMENT_TILING}.z * ({VARCONST_POSITION}.z + _Time.y * {FRAGMENT_SCROLL}.z))) * {FRAGMENT_TILING}.w;
{VARCONST_SDF} += frg;";
public override string SdfMethodName => "FragmentModifier";
public override ISDFEntity.SDFUniformField[] SdfUniformFields =>
new ISDFEntity.SDFUniformField[2]
{
new ISDFEntity.SDFUniformField(FRAGMENT_TILING, ISDFEntity.SDFUniformType.Float4),
new ISDFEntity.SDFUniformField(FRAGMENT_SCROLL, ISDFEntity.SDFUniformType.Float3)
};
public override void PushSdfEntityToShader(in Material raymarcherSessionMaterial, in string iterationIndex)
{
raymarcherSessionMaterial.SetVector(FRAGMENT_TILING + iterationIndex,
new Vector4(fragmentSharedData.fragmentTiling.x, fragmentSharedData.fragmentTiling.y, fragmentSharedData.fragmentTiling.z, fragmentSharedData.fragmentEvolution));
raymarcherSessionMaterial.SetVector(FRAGMENT_SCROLL + iterationIndex, fragmentSharedData.fragmentScroll);
}
public override bool ModifierSupportsSharedContainer => true;
public override object CreateSharedModifierContainer => new FragmentSharedData();
public override void PassModifierDataToSharedContainer()
{
if (!TryToGetSharedModifierContainerType<FragmentSharedData>(out var data))
return;
data.fragmentTiling = fragmentSharedData.fragmentTiling;
data.fragmentEvolution = fragmentSharedData.fragmentEvolution;
data.fragmentScroll = fragmentSharedData.fragmentScroll;
base.PassModifierDataToSharedContainer();
}
public override void PassSharedContainerDataToModifier()
{
if (!TryToGetSharedModifierContainerType<FragmentSharedData>(out var data))
return;
fragmentSharedData = data;
}
}