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;
[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;
}
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
{
public MatTestDataBuffer(in RMMaterialIdentifier materialIdentifier) : base(materialIdentifier)
{}
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct MaterialDataContainer
{
public float height;
public float blend;
public Vector3 colorBottom;
public Vector3 colorTop;
}
public MaterialDataContainer[] materialDataContainer;
public override Array GetComputeBufferDataContainer => materialDataContainer;
public override void InitializeDataContainerPerInstance(in IReadOnlyList<RMMaterialBase> materialInstances, in bool sceneObjectsAreUsingSomeInstances, in bool unpackedDataContainerDirective)
{
base.InitializeDataContainerPerInstance(materialInstances, sceneObjectsAreUsingSomeInstances, unpackedDataContainerDirective);
materialDataContainer = new MaterialDataContainer[materialInstances.Count];
}
public override (int dataLength, int strideSize) GetComputeBufferLengthAndStride()
=> (materialDataContainer.Length, sizeof(float) * 8);
public override void SyncDataContainerPerInstanceWithMaterialInstance(in int iterationIndex, in RMMaterialBase materialInstance)
{
MatTestScriptableBase material = (MatTestScriptableBase)materialInstance;
MaterialDataContainer container;
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);
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
{
public override string MaterialTypeName => "Custom Height Gradient Material";
public override MaterialUniformField[] MaterialUniformFieldsPerInstance
=> new MaterialUniformField[]
{
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)
};
public override MaterialMethodContainer MaterialMainMethod
=> new MaterialMethodContainer("CalculateHeightGradient",
@"
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);
}
");
public override string MaterialDataContainerTypePerInstance => nameof(MatTestDataBuffer.MaterialDataContainer);
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.
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