Implementing convincing glass surfaces on mobile hardware requires strict optimization to avoid render bottlenecks. Screen-space techniques like GrabPass introduce significant overhead due to frame buffer reads and sorting complexity. This approach bypasses those limitations by utilizing Material Capture (MatCap) textures within a single rendering pass, delivering reflections and refraction cues without runtime environment sampling.
Design Rationale & Precision Handling
The shader relies exclusively on geometric normals transformed into camera space, eliminating the need for tangent-space normal maps. This reduces texture fetches and simplifies the lighting pipeline. A critical consideration for mobile deploymetn involves vertex data precision. In networked or high-frequency synchronization scenarios (such as racing or parkour titles), reduced-precision types like half4 can cause vertex drift or jitter during rapid matrix updates. Consequently, all position calculations utilize float4 to maintain numerical stability. Projects with lower sync demands may safely revert to half-precision to conserve register pressure.
Shader Implementation
The following code establishes a transparent, single-pass material. It disables depth writing, configures alpha blending, and processes lighting through a unified vertex-fragment pipeline.
Shader "Custom/Mobile/GlassMatCap"
{
Properties
{
[Header(Tint & Intensity)]
_GlassTint("Refraction Tint", Color) = (0.5, 0.8, 1.0, 1.0)
_BaseIntensity("Primary Intensity", Float) = 1.0
[Header(Fresnel & Distortion)]
_RefractScale("Distortion Scale", Float) = 1.0
_FresnelPower("Fresnel Edge Strength", Float) = 1.0
[Header(MatCap Textures)]
_MatCapBase("Base Reflection Map", 2D) = "gray" {}
_MatCapHighlight("Specular/Refraction Map", 2D) = "black" {}
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
struct VertexInput
{
float4 vertex : POSITION;
half3 normal : NORMAL;
fixed2 uv : TEXCOORD0;
};
struct VertexOutput
{
float4 pos : SV_POSITION;
float4 worldPos : TEXCOORD1;
float3 normal : TEXCOORD2;
fixed2 uv : TEXCOORD3;
};
sampler2D _MatCapBase;
float4 _MatCapBase_ST;
sampler2D _MatCapHighlight;
float4 _MatCapHighlight_ST;
float4 _GlassTint;
float _BaseIntensity;
float _RefractScale;
float _FresnelPower;
VertexOutput Vert(VertexInput input)
{
VertexOutput output;
output.pos = UnityObjectToClipPos(input.vertex);
output.worldPos = mul(unity_ObjectToWorld, input.vertex);
output.normal = UnityObjectToWorldNormal(input.normal);
output.uv = TRANSFORM_TEX(input.uv, _MatCapBase);
return output;
}
fixed4 Frag(VertexOutput input) : SV_Target
{
// Transform normal to view space
float3 viewNormal = normalize(mul(UNITY_MATRIX_V, float4(input.normal, 0.0)).xyz);
// Calculate view direction for Fresnel
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(input.worldPos));
float ndv = abs(dot(normalize(input.normal), worldViewDir));
// Generate Fresnel term (1.0 at grazing angles, 0.0 at front)
float fresnel = 1.0 - ndv;
fresnel = smoothstep(0.0, _FresnelPower, fresnel);
// Map view-space normal to UV coordinates [0,1]
float2 baseUV = viewNormal.xy * 0.5 + 0.5;
// Apply angle-dependent distortion offset
float2 refractOffset = baseUV * (fresnel * _RefractScale);
// Sample primary MatCap for base diffusion/specular
float4 baseLayer = tex2D(_MatCapBase, baseUV) * _BaseIntensity;
// Sample secondary MatCap with distortion, apply tint blending
float4 highlightLayer = tex2D(_MatCapHighlight, baseUV + refractOffset);
highlightLayer.rgb = lerp(_GlassTint.rgb * 0.5, _GlassTint.rgb * highlightLayer.rgb, saturate(fresnel));
// Combine layers
float3 finalColor = baseLayer.rgb + highlightLayer.rgb;
// Alpha driven by base layer intensity and edge highlights
float alpha = saturate(max(baseLayer.r, fresnel * fresnel));
return fixed4(finalColor, alpha);
}
ENDCG
}
}
FallBack "Transparent/Diffuse"
}
Rendering Pipeline Breakdown
View-Space Coordinate Mapping: MatCap textures encode lighting relative to the camera. The fragment shader projects the geometric normal into view space, flips the Y-axis to align with standard UV conventions, and remaps the range to [0,1]. This generates stable sampling coordinates that remain consistent regardless of object rotation.
Fresnel-Driven Distortion: A dot product between the world normal and view direction produces a grazing-angle mask. This value simultaneously controls transparency alpha and acts as a multiplier for UV offset. As surfaces turn parallel to the camera, the offset increases, simulating stronger refraction and edge highlights without dynamic environment probes.
Layer Compositing: The primary MatCap layer provides the foundational surface response, scaled by an intensity parameter. The secondary layer introduces colored refraction effects, blended against a user-defined tint. Both layers are summed to produce the final RGB output, while the alpha channel ensures proper depth sorting and edge definition in transparent rendering queues.