HLSL Shaders
WARNING: The dxc
version that ships with Visual Studio 2022 does not support SPIR-V codegen!! You will need to either use the dxc
from the Vulkan SDK or build it from source.
Cacao Engine shaders should target Shader Model 6.0.
Matrix Packing
Shaders MUST use row-major matrix packing because the DirectX Shader Compiler changes that to column-major in SPIR-V, which is what Cacao Engine uses. See this section of the SPIR-V compatibility document for more info on why. Take a look at the whole of that document too; it will give better context as to writing HLSL for SPIR-V.
You can either mark each matrix with the row_major
qualifier, or add #pragma pack_matrix(row_major)
to the top of your file.
Compile Arguments
When using dxc
for shader compilation, you MUST ensure that the -fvk-use-gl-layout
flag is added to the command line, or else the engine may not be able to use your shader.
Main Functions
The main functions of all shader stages must have the name main
, otherwise Cacao Engine will fail to use them properly.
Inputs
All vertex shader inputs should be within a VSInput
struct. The struct can contain the following members:
Data Type |
Semantic |
---|---|
|
|
|
|
|
|
|
|
|
|
Cacao Engine Data
Cacao Engine sends engine data to shaders via a constant buffer. Below is an example of how this should be declared. The order of members is important!
struct CacaoGlobals {
float4x4 projection;
float4x4 view;
};
ConstantBuffer<CacaoGlobals> globals : register(b0);
Transform Matrix
All shaders must declare the transformation matrix as a push constant. This is done as follows:
struct Transformation {
mat4 transform;
};
[[vk::push_constant]] Transformation transform;
Local Object Data
All data for individual objects (material data) must be declared within another uniform block, which must be declared as follows:
struct ObjectData {
//Material data goes here...
};
ConstantBuffer<ObjectData> object;
Applying the Matrices
Since all of the matrices are row-major, apply them in the order below. This example assumes the output struct contains a member declared as float4 Pos : SV_POSITION;
, the output struct is named output
, the input struct contains a member declared as float3 Pos : POSITION0;
and that the input struct is named input
.
output.Pos = mul(input.pos, mul(transform.transform, mul(globals.view, globals.projection)));
Textures and Samplers
In HLSL, samplers and textures are separate objects. This model is not compatible with all APIs. A SamplerState
associated with a texture must have the same register number as that texture (e.g. register(t0)
on the texture is connected to the sampler with register(s0)
). SamplerState
s and textures must also all be marked with the [[vk::combinedImageSampler]]
annotation. In addition, you all textures must be given a binding number. This is different than HLSL registers, and they are shared between the vertex and fragment stages. Since binding 0
and 1
is used by the engine constant buffer, you must start with binding number 1
. Apply bindings with the [[vk::binding(NUMBER)]]
annotation. Here’s an example:
[[vk::binding(2)]] [[vk::combinedImageSampler]] Texture2D tex : register(t0);
[[vk::binding(2)]] [[vk::combinedImageSampler]] SamplerState texSampler : register(s0);
If dealing with all of that is a lot, just write a macro for all of that put together. Here’s one you can just copy-paste in:
#define CacaoTexture(name, reg) [[vk::binding(reg)]] [[vk::combinedImageSampler]] Texture2D name : register(t##reg);\
[[vk::binding(reg)]] [[vk::combinedImageSampler]] SamplerState name##Sampler : register(s##reg)
Then you can just do this instead: CacaoTexture(tex, 2);
For simplicity, you could place this macro in a texmacro.hlsl
file and #include
it for ease-of-use.