Raymarcher Technical & APi Documentation
Creating Custom Material
Namespace: Raymarcher.Materials
There is a possibility of creating custom materials, which involves a set of configurations and code writing.
I am planning to update the material creation process by introducing auto-generators, reducing the need for extensive code writing. Stay tuned for updates!

Every material in Raymarcher is defined by three key components:

- RMMaterialDataBuffer
Base class for a specific material type, managing material instances in the scene.

- RMMaterialBase
Main base scriptable object for a specific material instance in the Raymarcher, holding visually-representative material data.

- RMMaterialIdentifier
Main base class for a specific material identifier, defining all the required compile-time properties for custom materials.

Let's create a custom material that generates a linear gradient based on the currently marched position. This material type will be compatible with PC/Consoles platforms.




Create three scripts, each identical to the material types mentioned above (you can choose the names):
- MatTestDataBuffer (RMMaterialDataBuffer)
- MatTestIdentifier (RMMaterialIdentifier)
- MatTestScriptableData (RMMaterialBase)

In the MatTestScriptableData, declare fields that you will use on individual material instances and implement all the required abstract methods...
using UnityEngine;

using Raymarcher.Materials;

// Inherit from RMMaterialBase to create a scriptableObject. Add 'CreateAssetMenu' attribute to display this object in the Project window.
[CreateAssetMenu(fileName = nameof(MatTestScriptableBase), menuName = "Custom Raymarcher Material")]
public sealed class MatTestScriptableBase : RMMaterialBase
{
public MaterialData materialData = new MaterialData();

 [System.Serializable]
public class MaterialData
 {
  public float height = 0;
  public float blend = 0.5f;
  public Color colorBottom = Color.red;
  public Color colorTop = Color.blue;
 }

// We will come back to this method later!
public override RMMaterialDataBuffer MaterialCreateDataBufferInstance()
 {
  return null;
 }
}
In the MatTestDataBuffer, we need to declare commands for the compute buffer of this material, a structured container, and synchronization between material data and buffer data.
Take a look at the following code:
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;

using UnityEngine;

using Raymarcher.Materials;

public sealed class MatTestDataBuffer : RMMaterialDataBuffer
{
// You can allocate any data in the classes constructor if needed
public MatTestDataBuffer(in RMMaterialIdentifier materialIdentifier) : base(materialIdentifier)
 {}

 [Serializable, StructLayout(LayoutKind.Sequential)]
public struct MaterialDataContainer
 {
  public float height; // 4 bytes (1x float)
  public float blend; // 4 bytes (1x float)
  public Vector3 colorBottom; // rgb(no alpha) 12 bytes (3x floats)
  public Vector3 colorTop; // rgb(no alpha) 12 bytes (3x floats)
  // Total stride = 8x floats = 32 bytes = 256 bits
 }

// Declare an array of buffer containers - each element represents one material instance
public MaterialDataContainer[] materialDataContainer;

// Must always returns the buffer container ^
public override Array GetComputeBufferDataContainer => materialDataContainer;

// This is called everytime a new material instance is registered. Please do not forget to allocate your container with the number of material instances of this type
public override void InitializeDataContainerPerInstance(in IReadOnlyList<RMMaterialBase> materialInstances, in bool sceneObjectsAreUsingSomeInstances, in bool unpackedDataContainerDirective)
 {
  base.InitializeDataContainerPerInstance(materialInstances, sceneObjectsAreUsingSomeInstances, unpackedDataContainerDirective);
  // Allocate buffer array to material instances
  materialDataContainer = new MaterialDataContainer[materialInstances.Count];
 }

// As mentioned in the struct container = 8x size of floats. This is required for the computeBuffer.
public override (int dataLength, int strideSize) GetComputeBufferLengthAndStride()
  => (materialDataContainer.Length, sizeof(float) * 8); // Or use 'Marshal.SizeOf<YourDataStructureType>()'

// This is called every frame for every material instance of this type
public override void SyncDataContainerPerInstanceWithMaterialInstance(in int iterationIndex, in RMMaterialBase materialInstance)
 {
  // Cast your material type (the type is always safe as further material filtering is done before syncing)
  MatTestScriptableBase material = (MatTestScriptableBase)materialInstance;

  MaterialDataContainer container;

  // Sync all desired fields with the casted material instance
  container.height = material.materialData.height;
  container.blend = material.materialData.blend;
  Color colorTemp = material.materialData.colorBottom;
  container.colorBottom = new Vector3(colorTemp.r, colorTemp.g, colorTemp.b);
  colorTemp = material.materialData.colorTop;
  container.colorTop = new Vector3(colorTemp.r, colorTemp.g, colorTemp.b);

  // Sync every buffer element with individual material instance
  materialDataContainer[iterationIndex] = container;
 }
}
Now, we need to write a material identifier and the actual shader code so that the converter understands what specifically will be converted into the shading language.
using Raymarcher.Materials;

public sealed class MatTestIdentifier : RMMaterialIdentifier
{
// Could be any name. Just for a visual representation
public override string MaterialTypeName => "Custom Height Gradient Material";

// Clone the MaterialDataContainer data in the MatTestDataBuffer
public override MaterialUniformField[] MaterialUniformFieldsPerInstance
  => new MaterialUniformField[]
  {
   // !! Note that the order of declaration is exactly the same as in the MatTestDataBuffer.MaterialDataContainer !!
   new MaterialUniformField(nameof(MatTestDataBuffer.MaterialDataContainer.height), MaterialUniformType.Float),
   new MaterialUniformField(nameof(MatTestDataBuffer.MaterialDataContainer.blend), MaterialUniformType.Float),
   new MaterialUniformField(nameof(MatTestDataBuffer.MaterialDataContainer.colorBottom), MaterialUniformType.Float3),
   new MaterialUniformField(nameof(MatTestDataBuffer.MaterialDataContainer.colorTop), MaterialUniformType.Float3)
  };

// You can load the main method's content from resources/ text file or write it directly as a string.
// Note the method's datatype and parameters = these are constantly set as a main 'caller'
public override MaterialMethodContainer MaterialMainMethod
  => new MaterialMethodContainer("CalculateHeightGradient",
@"

// Here comes your shading code. Hence the naming conventions!
// Have a look at the Raymarcher's core shading library for proper use of 'Ray' struct and other properties/helpers:
// Raymarcher/CG/CoreLibrary/RMHLSL_Input.cginc
// and
// Raymarcher/CG/CoreLibrary/RMHLSL_Common.cginc

half4 CalculateHeightGradient(in MaterialDataContainer data, in Ray ray, half4 sdfRGBA)
{
half3 color = lerp(data.colorBottom, data.colorTop, smoothstep(data.height - data.blend, data.height + data.blend, ray.p.y));
return half4(color, 1);
}

");

// Could be any, but the name must match with the type in the shader, hence the 'in MaterialDataContainer data' parameter above...
public override string MaterialDataContainerTypePerInstance => nameof(MatTestDataBuffer.MaterialDataContainer);

// This material type is not using any textures
public override bool MaterialIsUsingTexturesPerInstance => false;
}
And finally, back in the MatTestScriptableBase, complete the very last method and implement all the classes that we have written to finalize the material.
// ... MatTestScriptableBase code ...
public override RMMaterialDataBuffer MaterialCreateDataBufferInstance()
 => new MatTestDataBuffer(new MatTestIdentifier());
Our material is now ready!

Create a new material instance by right-clicking in the Project Window, selecting your material, and assigning it to one of your SDF objects in the scene.

Experiment with the fields on your material instance and observe the different results.
You have created your very own Raymarcher material ready for multiple uses.

Primitives with Mandelbrot fractal using custom material instances