Custom Shaders

Similar to functions, classes, and properties in TypeScript, shader code also has its own set of APIs and配套的 UIScript. This article will guide you through customizing your shaders using these APIs and ShaderLab.

Quick Start

We'll start with the Unlit template to introduce our shader API. Follow the steps below to create a Unlit shader:

The engine will automatically generate the shader file and UIScript file for you:

By default, the Unlit template includes skinning calculations and a Shadow Pass. As shown below, skeletal animations and shadows render correctly:

Key code includes using UsePass "pbr/Default/ShadowCaster" to enable shadow mapping and getSkinMatrix to animate the mesh:

UsePass "pbr/Default/ShadowCaster"
 
Pass "Example" {
	#include "Skin.glsl"
 
	Varyings vert(Attributes attr) {
		Varyings v;
 
		vec4 position = vec4(attr.POSITION, 1.0);
 
		// Skin
		#ifdef RENDERER_HAS_SKIN
		  mat4 skinMatrix = getSkinMatrix(attr);
		  position = skinMatrix * position;
		#endif
 
		gl_Position = renderer_MVPMat * position;
		v.uv = attr.TEXCOORD_0;
 
		return v;
	}
}

Unlit shaders are unaffected by lighting by default. To make the output respond to lighting, use APIs from Light.glsl:

#include "Light.glsl"
 
// Demo: Calculate only the first directional light
DirectLight light = getDirectLight(0);
float dotNL = saturate(dot(v.normalWS, -light.direction));
baseColor.rgb *= dotNL * light.color;

While vertex color calculations, normal maps, and ambient lighting can also be implemented, we recommend using the PBR template instead of the Unlit template. The PBR template already includes these features and provides a more comprehensive lighting model (e.g., anisotropy, Clear Coat) with macro-based extensions.

PBR Template

Create a PBR Shader template and bind it to your material. The material panel will now include settings for base properties, metallic/roughness, anisotropy, normals, emissive, occlusion, and Clear Coat, all responsive to direct and ambient lighting:

Overriding the Lighting Model

  1. Create a DemoPass.glsl file and include it in your main shader:
// PBRShader.gs
SubShader "Default" {
	Pass "Forward Pass" {
	  VertexShader = PBRVertex;
	  FragmentShader = PBRFragment;
 
	  // #include "ForwardPassPBR.glsl"
	  #include "./DemoPass.glsl"
	}
}
  1. Modify the lighting model in DemoPass.glsl (demo shows direct light changes):
// DemoPass.glsl
#include "Common.glsl"
#include "Fog.glsl"
 
#include "AttributesPBR.glsl"
#include "VaryingsPBR.glsl"
// #include "LightDirectPBR.glsl"
#include "DemoLight.glsl"
 
#include "LightIndirectPBR.glsl"
 
#include "VertexPBR.glsl"
#include "FragmentPBR.glsl"
  1. Override the specular reflection model using FUNCTION_SPECULAR_LOBE (example uses thin-film interference):
// DemoLight.glsl
#define FUNCTION_SPECULAR_LOBE specularLobe_iridescence
 
#include "BRDF.glsl"
#include "./IridescenceFunction.glsl"
 
void specularLobe_iridescence(Varyings varyings, SurfaceData surfaceData, BRDFData brdfData, vec3 incidentDirection, vec3 attenuationIrradiance, inout vec3 specularColor){
 vec3 thin = DirectBDRFIridescence(surfaceData, incidentDirection, brdfData);
 vec3 BRDF_Specular = BRDF_Specular_GGX( incidentDirection, surfaceData, surfaceData.normal, brdfData.specularColor, brdfData.roughness);
 vec3 factor = mix(BRDF_Specular, thin, material_Iridescence);
 specularColor += attenuationIrradiance * factor;
}
 
#include "LightDirectPBR.glsl"

Common APIs

Usage Example

#include "Common.glsl"
 
float f2 = pow2(0.5);

Common

Provides macros like PI and utility functions (gammaToLinear, pow2). See source.

Fog

Depth fog calculation:

vec4 fog(vec4 color, vec3 positionVS);

Transform

System variables for model/view/world space:

mat4 renderer_LocalMat;
mat4 renderer_ModelMat;
mat4 camera_ViewMat;
mat4 camera_ProjMat;
mat4 renderer_MVMat;
mat4 renderer_MVPMat;
mat4 renderer_NormalMat;
 
vec3 camera_Position;
vec3 camera_Forward;
vec4 camera_ProjectionParams;

Light

Access engine lighting data:

DirectLight getDirectLight(int index);
PointLight getPointLight(int index);
SpotLight getSpotLight(int index);
 
EnvMapLight scene_EnvMapLight;
 
#ifdef SCENE_USE_SH
    vec3 scene_EnvSH[9];
#endif
 
#ifdef SCENE_USE_SPECULAR_ENV
    samplerCube scene_EnvSpecularSampler;
#endif

Normal

Normal calculation utilities:

vec3 getNormalByNormalTexture(mat3 tbn, sampler2D normalTexture, float normalIntensity, vec2 uv, bool isFrontFacing);
mat3 getTBNByDerivatives(vec2 uv, vec3 normal, vec3 position, bool isFrontFacing);

Shadow

Shadow-related functions (source):

int computeCascadeIndex(vec3 positionWS);
vec3 getShadowCoord(vec3 positionWS);
float sampleShadowMap(vec3 positionWS, vec3 shadowCoord);

Skin

Skinning calculation:

mat4 getSkinMatrix(Attributes attributes);

BlendShape

Blend shape calculation:

void calculateBlendShape(Attributes attributes, inout vec4 position, inout vec3 normal, inout vec4 tangent);

PBR APIs

AttributesPBR

All PBR attribute variables (source).

VaryingsPBR

All PBR varying variables (source).

LightDirectPBR

Direct lighting calculations based on BRDF (source).

Usage:

evaluateDirectRadiance(varyings, surfaceData, brdfData, shadowAttenuation, color.rgb);

Override Macros:

#define FUNCTION_SURFACE_SHADING surfaceShading
#define FUNCTION_DIFFUSE_LOBE diffuseLobe
#define FUNCTION_SPECULAR_LOBE specularLobe
#define FUNCTION_CLEAR_COAT_LOBE clearCoatLobe
#define FUNCTION_SHEEN_LOBE sheenLobe
 
// Function signatures...
See PBR template extension example above.

LightIndirectPBR

Indirect lighting (IBL) calculations (source).

Usage:

evaluateIBL(varyings, surfaceData, brdfData, color.rgb);

Override Macros:

#define FUNCTION_DIFFUSE_IBL evaluateDiffuseIBL
#define FUNCTION_SPECULAR_IBL evaluateSpecularIBL
#define FUNCTION_CLEAR_COAT_IBL evaluateClearCoatIBL
#define FUNCTION_SHEEN_IBL evaluateSheenIBL
 
// Function signatures...

VertexPBR

Vertex shader utilities for UV/TBN/skinning (source):

Varyings varyings;
varyings.uv = getUV0(attributes);
 
VertexInputs vertexInputs = getVertexInputs(attributes);
 
// positionWS
varyings.positionWS = vertexInputs.positionWS;
 
// normalWS、tangentWS、bitangentWS
#ifdef RENDERER_HAS_NORMAL
  varyings.normalWS = vertexInputs.normalWS;
  #ifdef RENDERER_HAS_TANGENT
    varyings.tangentWS = vertexInputs.tangentWS;
    varyings.bitangentWS = vertexInputs.bitangentWS;
  #endif
#endif
 
gl_Position = renderer_MVPMat * vertexInputs.positionOS;

BRDF

Core PBR lighting calculations and data structures (source).

BTDF

Transmission/refraction functions (source).

FragmentPBR

Handles material properties and surface data initialization (source):

BRDFData brdfData;
 
SurfaceData surfaceData = getSurfaceData(varyings, aoUV, gl_FrontFacing);
initBRDFData(surfaceData, brdfData);

Final Notes

For complete file organization examples, refer to the official ForwardPassPBR implementation.

Was this page helpful?