Unity3技术之表面着色

谁都知道在Unity中可以编写shader,可是他们很难写,尤其当你需要每个像素的灯光和阴影产生交互.在Unity3中,除了要包含旧的支持,你的shader也必须支持新的递延照明渲染(Deferred Lighting).所以我们决定要让shader更容易写!

一年多以前,我曾认为”着色器马死的快(呵呵,着色器必死,相关的帖子http://aras-p.info/blog/2009/05/05/shaders-must-die/,http://aras-p.info/blog/2009/05/07/shaders-must-die-part-2/,http://aras-p.info/blog/2009/05/10/shaders-must-die-part-3/)”.而现在我们正在开发Unity3,我们称之为表面着色(Surface Shaders),因为我觉得”着色器马死的快”做个这个新物种的名称不会得到老板认同.

主题思想就是在90%的情况下我之需要声明表面的特性就可以了.以后就想说:

嘿,反照率来自这个纹理和那个纹理的混合以及法线贴图.使用Blinn-Phong照明模式,别再来烦我了嗷!

像上面说的,我没有理会是否要提前或者延迟渲染,或者如何使用各种灯的类型去处理,或者要设置有多少效果将用灯光来呈现,或者用将有多少会产生实时阴影的部分,等等都不是我感兴趣的!这些肮脏的渲染工作怎么能让程序去做?!得让程序员解脱!

当然这不是一个新概念.大部分图形渲染编辑器都没用像素色(pixel color)去作为最后输出的节点,而是用了他们的一些可以描述他们的表面参数(弥漫,镜面,法线)作为节点,所有的灯光代码也不是针对作色器本身图形.OpenShadingLanguage(http://code.google.com/p/openshadinglanguage/)是和我们想的类似的一个想法,不过因为它是针对电影的脱机渲染,所有它更丰富更复杂.

下面是一个简单的例子,但它很完整,演示了Unity3的漫反射贴图和法线贴图的Shader.

Shader "Example/Diffuse Bump" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _BumpMap ("Bumpmap", 2D) = "bump" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
        float2 uv_MainTex;
        float2 uv_BumpMap;
      };
      sampler2D _MainTex;
      sampler2D _BumpMap;
      void surf (Input IN, inout SurfaceOutput o) {
        o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
        o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
      }
      ENDCG
    }
    Fallback "Diffuse"
  }

将它赋予给漂亮的模型和贴图,它可以产生漂亮的图像!怎能不酷?!

这个是不是真的很有趣,将着色器的复杂语法简单化,后面再为一些老古董机器做一些后备声明.剩下的只有CG语言/HLSL的代码,然后通过自动生成最终代码提高图像效果.

我们来解析一下这段着色器代码:

  • #pragma surface surf Lambert:这是一个表面着色(surf)用的主要函数,和用Lambert照明模式.
  • struct Input:表面作色器输入的数据.这可以有多种定义,用来描述每个顶点和像素的对应关系.上面这段代码里它有两个纹理坐标.
  • surf函数,表面的实际着色代码.需要由SurfaceOutput(预定义结构)写入.它可以写入自定义结构,实际的代码只是写反照率和法线的输出.

什么是生成?

Unity的”表面着色器代码生成器”将利用这个方式产生实际的顶点和像素着色器,并将它编译好给各种目标平台.在Unity3中将默认支持这种着色器:

  • 提前渲染和递延渲染
  • 预计算对象物体有无光照.
  • 平行光,点光源,射灯的cookie阴影有无,实时光影的有无.
  • 对于提前渲染,它会在编译时计算每个顶点的灯的效果反映.
  • 延迟照明将会产生法线贴图,镜面贴图最后通过灯光的反射率和任何其它光照光影的反映.
  • 它可以选择性的生成一个阴影,比如一些复杂的通道贴图产生的阴影.

例如下面的代码通过提供的一个方向灯,4个顶点光源,三个阴影光被编译为一个提前渲染的可选光影的shader.我建议你滚动页面往下看看就好了:

#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma fragmentoption ARB_fog_exp2
#pragma fragmentoption ARB_precision_hint_fastest
#pragma multi_compile_fwdbase
#include "HLSLSupport.cginc"
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct Input {
    float2 uv_MainTex : TEXCOORD0;
};
sampler2D _MainTex;
sampler2D _BumpMap;
void surf (Input IN, inout SurfaceOutput o)
{
    o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
    o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_MainTex));
}
struct v2f_surf {
  V2F_POS_FOG;
  float2 hip_pack0 : TEXCOORD0;
  #ifndef LIGHTMAP_OFF
  float2 hip_lmap : TEXCOORD1;
  #else
  float3 lightDir : TEXCOORD1;
  float3 vlight : TEXCOORD2;
  #endif
  LIGHTING_COORDS(3,4)
};
#ifndef LIGHTMAP_OFF
float4 unity_LightmapST;
#endif
float4 _MainTex_ST;
v2f_surf vert_surf (appdata_full v) {
  v2f_surf o;
  PositionFog( v.vertex, o.pos, o.fog );
  o.hip_pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
  #ifndef LIGHTMAP_OFF
  o.hip_lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
  #endif
  float3 worldN = mul((float3x3)_Object2World, SCALED_NORMAL);
  TANGENT_SPACE_ROTATION;
  #ifdef LIGHTMAP_OFF
  o.lightDir = mul (rotation, ObjSpaceLightDir(v.vertex));
  #endif
  #ifdef LIGHTMAP_OFF
  float3 shlight = ShadeSH9 (float4(worldN,1.0));
  o.vlight = shlight;
  #ifdef VERTEXLIGHT_ON
  float3 worldPos = mul(_Object2World, v.vertex).xyz;
  o.vlight += Shade4PointLights (
    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
    unity_LightColor0, unity_LightColor1, unity_LightColor2, unity_LightColor3,
    unity_4LightAtten0, worldPos, worldN );
  #endif // VERTEXLIGHT_ON
  #endif // LIGHTMAP_OFF
  TRANSFER_VERTEX_TO_FRAGMENT(o);
  return o;
}
#ifndef LIGHTMAP_OFF
sampler2D unity_Lightmap;
#endif
half4 frag_surf (v2f_surf IN) : COLOR {
  Input surfIN;
  surfIN.uv_MainTex = IN.hip_pack0.xy;
  SurfaceOutput o;
  o.Albedo = 0.0;
  o.Emission = 0.0;
  o.Specular = 0.0;
  o.Alpha = 0.0;
  o.Gloss = 0.0;
  surf (surfIN, o);
  half atten = LIGHT_ATTENUATION(IN);
  half4 c;
  #ifdef LIGHTMAP_OFF
  c = LightingLambert (o, IN.lightDir, atten);
  c.rgb += o.Albedo * IN.vlight;
  #else // LIGHTMAP_OFF
  half3 lmFull = DecodeLightmap (tex2D(unity_Lightmap, IN.hip_lmap.xy));
  #ifdef SHADOWS_SCREEN
  c.rgb = o.Albedo * min(lmFull, atten*2);
  #else
  c.rgb = o.Albedo * lmFull;
  #endif
  c.a = o.Alpha;
  #endif // LIGHTMAP_OFF
  return c;
}

在90行的代码中,10行是原始的着色器代码,其余的代码在Unity2的时候几乎都要手写的.别急,这只是写了一点提前渲染的部分!还需要写递延渲染,可选阴影等等灯.

因此,现在应该会比较容易的写着色器(对于我来说至少).Unity的用户也愿意少写代码,少些至少三倍的代码何乐不为?它还会有一些改变以适应Unity下一步新的照明系统.

预定义的输入值

输入结构可以包含纹理坐标和一些预定义的值,例如查看的方向,世界空间的位置,世界空间的反射矢量灯.例如,如果你利用世界空间反射矢量(如自发光),在模型表面做一些CubeMap反射效果(递延渲染的反射将不会计算,因为它不会产生效果,所以也不需要考虑反射向量),

用一个小例子说明做简单的灯光渲染:

#pragma surface surf Lambert
  struct Input {
      float2 uv_MainTex;
      float2 uv_BumpMap;
      float3 viewDir;
  };
  sampler2D _MainTex;
  sampler2D _BumpMap;
  float4 _RimColor;
  float _RimPower;
  void surf (Input IN, inout SurfaceOutput o) {
      o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
      o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
      half rim =
          1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
      o.Emission = _RimColor.rgb * pow (rim, _RimPower);
  }

顶点修改器

它可以自定义顶点修饰功能,用来修改或者产生每个顶点的数据.像风吹动树的动画,草的摇摆等等.

我最喜欢沿着法线顶点的方向去移动顶点.像打肿了的效果…

自定义光照模型

有一个内置的简单的光照模型,你也可以指定你自己的.A照明模式只不过将常用的光的参数,方向衰减,而没包含一些提前渲染和延迟渲染以及一些不长用到的方法.因此,对于任何奇特的效果你都要通过提供自定义光照模型来完成.

例如wrapped-Lambert光照模式:
  #pragma surface surf WrapLambert
  half4 LightingWrapLambert (SurfaceOutput s, half3 dir, half atten) {
      dir = normalize(dir);
      half NdotL = dot (s.Normal, dir);
      half diff = NdotL * 0.5 + 0.5;
      half4 c;
      c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);
      c.a = s.Alpha;
      return c;
  }
  struct Input {
      float2 uv_MainTex;
  };
  sampler2D _MainTex;
  void surf (Input IN, inout SurfaceOutput o) {
      o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
  }

幕后

我们使用Ryan Gordon的mojoshder(http://hg.icculus.org/icculus/mojoshader/)中的HLSL解析器来分析原始的表面着色器代码.找出有哪些成员结构,有哪些函数原型等等.在这个阶段做的一些错误检查是为了告诉用户它的面函数原型是错误或者他的结构缺少必要的成员.

为了弄清楚着色器实际的照明关系,我们做了像素着色器的小工具来编译CG语言和使用CG语言的API查询使用了哪些输入输出.这样我们可以算出例如一个法线贴图实际不用递延照明并也节省了一些顶点着色器指令和一个纹理坐标值.

生成的代码最终根据目标平台被编译.Cg为Windows/Mac,XDK HLSL为Xbox 360,PS3 Cg为PS3,和我们自己创造的HLSL2GLSL为iPhone, Android和即将退出的Unity NativeClient(http://blogs.unity3d.com/2010/05/19/google-android-and-the-future-of-games-on-the-web/).

就这样,我们会继续做下去,等到Unity3发布的时候,我希望更多的人会尝试写着色器!

转载请注明来自1Vr.Cn,原文地址:http://blogs.unity3d.com/2010/07/17/unity-3-technology-surface-shaders/

发布于 :未分类

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注