Shaders must die, part 2
I started playing around with the idea of “shaders must die“. I’m experimenting with extracting “surface shaders” for now.
Right now my experimental pipeline is:
- Write a surface shader file
- Perl script transforms it into Unity 2.x shader file
- Which in turn is compiled by Unity into all lighting/shadows permutations, for D3D9 and OpenGL backends. Cg is used for actual shader compilation.
I have very simple cases working. For example:
Properties
2D _MainTex
EndProperties
Surface
o.Albedo = SAMPLE(_MainTex);
EndSurface
This is a “no bullshit” source code for a simple Diffuse (Lambertian) shader, 87 bytes of text.
The Perl script produces a Unity 2.x shader. This will be long, but bear with me – I’m trying to show how much stuff has to be written right now, when we’re operating on vertex/pixel shader level. See Attenuation and Shadows for Pixel Lights in Unity docs for how this system works.
Shader "ShaderNinja/Diffuse" {
Properties {
_MainTex ("_MainTex", 2D) = "" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
Blend AppSrcAdd AppDstAdd
Fog { Color [_AddFog] }
Pass {
Tags { "LightMode"="PixelOrNone" }
CGPROGRAM
#pragma fragment frag
#pragma fragmentoption ARB_fog_exp2
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
uniform sampler2D _MainTex;
struct v2f {
float2 uv_MainTex : TEXCOORD0;
};
struct f2l {
half4 Albedo;
};
half4 frag (v2f i) : COLOR0 {
f2l o;
o.Albedo = tex2D(_MainTex,i.uv_MainTex);
return o.Albedo * _PPLAmbient * 2.0;
}
ENDCG
}
Pass {
Tags { "LightMode"="Pixel" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_builtin
#pragma fragmentoption ARB_fog_exp2
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
#include "AutoLight.cginc"
struct v2f {
V2F_POS_FOG;
LIGHTING_COORDS
float2 uv_MainTex;
float3 normal;
float3 lightDir;
};
uniform float4 _MainTex_ST;
v2f vert (appdata_tan v) {
v2f o;
PositionFog( v.vertex, o.pos, o.fog );
o.uv_MainTex = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normal = v.normal;
o.lightDir = ObjSpaceLightDir(v.vertex);
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
uniform sampler2D _MainTex;
struct f2l {
half4 Albedo;
half3 Normal;
};
half4 frag (v2f i) : COLOR0 {
f2l o;
o.Normal = i.normal;
o.Albedo = tex2D(_MainTex,i.uv_MainTex);
return DiffuseLight (i.lightDir, o.Normal, o.Albedo, LIGHT_ATTENUATION(i));
}
ENDCG
}
}
Fallback "VertexLit"
}
Phew, that is quite some typing to get simple diffuse shader (1607 bytes)! Well, at least all the lighting/shadow combinations are handled by Unity macros here. When Unity takes this shader and compiles into all permutations, it results in 58 kilobytes of shader assembly (D3D9 + OpenGL, 17 light/shadow combinations).
Let’s try something slightly different: bumpmapped, with a detail texture:
Properties
2D _MainTex
2D _Detail
2D _BumpMap
EndProperties
Surface
o.Albedo = SAMPLE(_MainTex) * SAMPLE(_Detail) * 2.0;
o.Normal = SAMPLE_NORMAL(_BumpMap);
EndSurface
This is 173 bytes of text. Generated Unity shader is 2098 bytes, which compiles into 74 kilobytes of shader assembly.
In this case, the processing script detects that surface shader modifies normal per pixel, and does the necessary tangent space light transformations. It all just works!
So this is where I am now. Next up: detect which lighting model to use based on surface parameters (right now it always uses Lambertian). Fun!
Very smart… I’m excited to see how this pans out. Getting more into the Renderman mode is smart. It’s a proven model, a lot more mature than the current shader models games use.
although I can barely understand this, I still sense you’re on a very right path. this just feels right! :)
This is a pretty interesting way of doing things. Why do developers insist on reinventing the wheel in even more complex ways all the time when a small simple solution like this one is just waiting to be discovered? :)
Technically this is all very impressive, but I do have an slight issue with the assertion that a Renderman approach is somehow more ‘natural’ and not driven by technology. In fact the approach was driven by the way the Renderman renderer worked, in the same way real-time shaders are driven by the way their renderers work. Neither one is really a ‘purer’ form, they’re just different because they evolved from different base techniques.
I do agree that for most ‘normal’ render paths an abstraction like this could be useful though, although my inherent distrust of passing code through too many compilers / translators / generators would make me scrutinise the results very closely.
@steve: I don’t know; Renderman just “feels” more natural. Yes, it does have both surface properties and lighting response in a single shader, so it’s not like “totally separated out”.
My issue with current way of writing shaders is that it’s very much tied into how things are operating. For example, if you have a simple textured surface with Lambertian BRDF – you still have to write shaders differently if you’re doing single pass per light forward renderer, or multiple lights per pass forward renderer, or hybrid renderer, etc. Even though there’s no good reason to do that; it should be enough just to state “albedo comes form texture” and “use Lambertian BRDF”.
So that’s what I’m playing around with. What I have so far does not do any fancy translation/generation/compilation, it just merely pastes all the boilerplate code around (i.e. it produces pretty much the same shader as it would be hand-written). In majority of cases, lots of things done in a shader are really standard (transform position, compute fog, set up blending modes, transform UVs, do tangent space transforms, pass down light vectors etc.).
[...] « Shaders must die, part 2 [...]
Yeah, I agree that there are common calculations that you would do regardless of the number of passes you do it in or whether you’re doing forward or deferred rendering, but then we usually use utility functions for that. From what you’re saying then, this sounds like a more friendly way to modularise regular shader code, at the macro end (standardising the phases) and at the detail end (which lighting model to use) – which definitely sounds useful in most normal render paths. It probably just starts to get trickier when you start adding things like domain-specific animation (e.g. foliage), but then I guess you would just write a different macro-level set up for those kinds of things.
Interesting stuff anyway!
Yeah. The common approach is using library functions or macros. What I am trying to do is just inverting the whole process: usually you put macros/functions inside of your shader code. I’m trying to automatically put macros/functions around shader snippets. Also, to separate out surface properties from the BRDF.
I have no idea if this will lead to anything, but hey, it’s fun at least!
This coupled with the option to specify all of the things like culling, blending, ztest, zwrite, alpha test, and fog would be very very useful for the majority of unity users.