如果你是一个shader编程的新手,并且你想学到下面这些酷炫的技术,我觉得你可以看看这篇教程:

  • 实现一个积雪效果的shader
  • 创建一个具有凹凸纹理的shader
  • 为每个像素修改其对应纹理值
  • 在表面着色器中修改模型的顶点数据

引论

这是我们系列教程的第二部分,我们将在此部分实现些有用的技术。在学习完第一部分的所有背景知识后,我们将利用所学的知识实现一个简单的积雪效果的shader。效果如下:

准备工作

我们想做的其实很简单,简单介绍一下:

  • 随着Snow Level(表示积雪的程度,该值越大,积雪越深)的增大,我们将迎着下雪方向的岩石纹理区域都变成Snow Color(雪的颜色)
  • 随着Snow Level的增大,我们想将该岩石模型稍微变大一点,尤其是雪的边缘厚度要增加,这样给人真正的“积”雪效果。

步骤1 – Bumped Diffuse Shader

我们创建一个新的Diffuse shader(默认新建的shader基本都是Diffuse shader),并向其中添加凹凸纹理效果(来自猫大的原话:法线贴图是凸凹贴图(Bump mapping)的一种常见应用,简单说就是在不增加模型多边形数量的前提下,通过渲染暗部和亮部的不同颜色深度,来为原来的贴图和模型增加视觉细节和真实效果。简单原理是在普通的贴图的基础上,再另外提供一张对应原来贴图的,可以表示渲染浓淡的贴图。通过将这张附加的表示表面凸凹的贴图的因素于实际的原贴图进行运算后,可以得到新的细节更加丰富富有立体感的渲染效果。)。

Shader "Custom/SnowShader" {Properties {_MainTex ("Base (RGB)", 2D) = "white" {}//新的凹凸纹理贴图_Bump ("Bump", 2D) = "bump" {}}SubShader {Tags { "RenderType"="Opaque" }LOD 200CGPROGRAM#pragma surface surf Lambertsampler2D _MainTex;//必须添加一个与Properties代码区中的同名的_Bump变量,作为Properties中_Bump的引用。
        //具体缘由详见教程第一部分。
         sampler2D _Bump;struct Input {float2 uv_MainTex;//用来得到_Bump的uv坐标
              float2 uv_Bump;};void surf (Input IN, inout SurfaceOutput o) {half4 c = tex2D (_MainTex, IN.uv_MainTex);//从_Bump纹理中提取法向信息o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump));o.Albedo = c.rgb;o.Alpha = c.a;}ENDCG}FallBack "Diffuse"
}

Unity已经在新建的shader中帮我们自动添加了很多代码,我们只要添加凹凸纹理相关的代码即可。

在上述代码,我们完成了一下内容:

  • 定义了默认值为“bump”的2D类型的纹理_Bump,初始值为空。(默认值为“bump”的2D类型纹理本质就是凹凸纹理类型)
  • 在CG代码区域中(CGPROGRAM…ENDCG)中添加了与_Bump同名的sampler2D变量,该sampler2D _Bump做的事情就是再次声明并链接了_Bump,使得接下来的CG代码区域能够使用这个变量。
  • 在Input结构体中添加一个变量float2 uv_Bump来获得_Bump纹理的uv坐标。
  • 在surf函数中添加UnpackNormal函数来获取对应像素的法向值(因为该surf函数时每个像素调用一次)。注意我们先使用tex2D函数来计算当前像素的具体值(tex2D作用仅仅是通过一个二维uv坐标在纹理上获取该处值,根据纹理的类型不同,获取的值的含义也不一样,比如bump类型纹理上存储的值代表的含义是该点的法向量,而普通纹理一般代表的是该点的颜色值),具体过程是根据对应uv坐标(IN.uv_Bump)在bump类型纹理(_Bump)上得到该点存储的值,注意此时得到的值是一个fixed4的值,但是法向值只需要fixed3类型,所以需要UnpackNormal进行转换(Unity3d的标准法线解压函数 — fixed3 UnpackNormal(fixed4 packednormal))。

经过上述步骤,我们得到了一个很普通的bumped shader。

我们将该shader应用到我们的模型上,并赋予相对应的纹理贴图,具体代码和资源请戳这里。

我在SnowRock文件夹下创建了名为SnowRock1的材质和shader,将我们的代码粘贴到SnowRock1.shader中,并将该shader拖到SnowRock1材质上。

随后我们将Free_Rocks –> _models中的cliff拖到Scene面板中,再将SnowRock1材质赋给该模型。此时我们还看不出任何变化,因为我们还没有给shader中Properties中那两个纹理变量赋值。

此时我们可以看到岩石效果的变化。

为了体现bump纹理的作用,我们将使用bump纹理和不使用bump纹理的岩石做下对比。

步骤2 – 在岩石上添加些雪

接下来我们要做的就是计算对应像素的法向量是否与下雪的方向一致(如果一致,那么就将该像素置为雪的颜色,这个道理和平行光的原理很类似)。

我们使用点乘(dot product)计算方向是否一致。两个单位向量之间的点乘等于这两个向量之间夹角的余弦值。CG中自带了一个dot函数为我们做两向量的点乘计算。这样的话,当我们计算出的点乘结果为1,则说明两向量间余弦值为1,即两者夹角为0,说明两者方向一致,而结果为-1时,说明两者方向相反。所以我们不必算出最后地角度,仅仅通过该点乘结果就可以判断两向量之间方向的关系。

单位向量是指大小为1的向量,也就是说当向量v满足sqrt(v.x*v.x+v.y*v.y+v.z*v.z)=1时,此向量为单位向量。不要认为单位向量就是(1,1,1)啊!!!

如果要使用点乘的结果来表示两向量之间的角度,首先要将两个向量的长度变为单位长度,也就是将两个向量变到单位向量再计算点乘。

知道这些后,我们在shader中定义以下属性。

Properties {_MainTex ("Base (RGB)", 2D) = "white" {}_Bump ("Bump", 2D) = "bump" {}_Snow ("Snow Level", Range(0,1) ) = 0_SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)_SnowDirection ("Snow Direction", Vector) = (0,1,0)_SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1
}

在上面代码中定义了一下变量:

  • _Snow 表示覆盖在岩石上雪的数量,范围从0~1.
  • _SnowColor 表示雪的颜色,默认为白色。
  • _SnowDirection 注意!!!此处表示的其实是雪落下方向的反向,比如我们默认雪是从天空直接落下来的,理论上应该为(0,-1,0),但是此处取得是它的反向,用的是(0,1,0)
  • _SnowDepth 我们将在步骤3中使用到该变量,它表示雪的厚度,范围从0~0.3

根据第一部分所学的内容,我们接着在下面的CGPROGRAM…ENDCG代码中添加如下变量:

sampler2D _MainTex;
sampler2D _Bump;
float _Snow;
float4 _SnowColor;
float4 _SnowDirection;
float _SnowDepth;

现在,除了纹理以外,我们把其他属性都当做float类型进行处理。

接着我们要更新我们shader中的Input结构体。此处要注意,我们不能直接用每个像素的法向值,因为法向贴图(normal map)为我们计算的每个像素法向值是在切空间的法向值,所以我们必须重新计算出每个像素在世界坐标系下真正的法向值,以此来和SnowDirection进行比较来决定此处是否覆盖上雪的颜色。

得到世界坐标系下的法向量还是有点麻烦的,为此我还特意看了下官方文档。因为我们的表面着色器写入了o.Normal,所以按照第一部分的讲解,我们需要使用INTERNAL_DATA来计算世界坐标系下的法向量,此处我们使用了WorldNormalVector函数。

我们将这些数据变量放到Input结构体中:

struct Input {float2 uv_MainTex;float2 uv_Bump;float3 worldNormal;INTERNAL_DATA};

最后我们完善surf函数,结束我们的shader程序。

void surf (Input IN, inout SurfaceOutput o) {//该像素的真实颜色值half4 c = tex2D (_MainTex, IN.uv_MainTex);//从凹凸贴图中得到该像素的法向量o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));//得到世界坐标系下的真正法向量(而非凹凸贴图产生的法向量,要做一个切空间到世界坐标系的转化)和雪落//下相反方向的点乘结果,即两者余弦值,并和_Snow(积雪程度)比较if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow))//此处我们可以看出_Snow参数只是一个插值项,当上述夹角余弦值大于//lerp(1,-1,_Snow)=1-2*_Snow时,即表示此处积雪覆盖,所以此值越大,//积雪程度程度越大。此时给覆盖积雪的区域填充雪的颜色o.Albedo = _SnowColor.rgb;else//否则使用物体原先颜色,表示未覆盖积雪 o.Albedo = c.rgb;o.Alpha = 1;
}

现在你们肯定迫切想知道if语句中到底做了哪些事情?

  • 得到雪落下相反方向的向量和该点世界坐标系下的法向量之间的点乘结果。此处注意使用的法向值是利用WorldNormalVector函数将切空间的法向值转化到世界坐标系下而得来的,另外提一点,切空间的法向值其实就是所使用的凹凸贴图法向值。
  • 经过点乘之后,我们得到一个1到-1之间的值,越接近于1,说明该点法向与雪落下相反方向越一致,当为-1时,说明两者方向相反。
  • 然后我们将点乘结果和一个插值结果进行比较,该插值使用了函数lerp(1,-1,_Snow)。实际上该函数结果为1+(-1-1)*_Snow=1-2*_Snow,所以当_Snow为1时,函数结果为-1,而当_Snow为0时,函数结果为1。注意为了符合正常的自然现象,我们_Snow一般只取0~0.5,因为大于0.5时,插值的结果将小于0,会造成雪好像穿过了岩石,落到了岩石的后面。这个道理和光照的道理一样,物体背面是见不到阳光的。
  • 当该点求得的点乘结果大于插值范围,将显示积雪颜色,反之显示原先的纹理颜色。

下面使我们完整的积雪效果shader:

Shader "Custom/SnowShader2" {Properties {_MainTex ("Base (RGB)", 2D) = "white" {}_Bump ("Bump", 2D) = "bump" {}_Snow ("Snow Level", Range(0,1)) = 0_SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)_SnowDirection ("Snow Direction", Vector) = (0,1,0)_SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1}SubShader {Tags { "RenderType"="Opaque" }LOD 200CGPROGRAM#pragma surface surf Lambertsampler2D _MainTex;sampler2D _Bump;float _Snow;float4 _SnowColor;float4 _SnowDirection;float _SnowDepth;struct Input {float2 uv_MainTex;float2 uv_Bump;float3 worldNormal;INTERNAL_DATA};void surf (Input IN, inout SurfaceOutput o) {//该像素的真实颜色值half4 c = tex2D (_MainTex, IN.uv_MainTex);//从凹凸贴图中得到该像素的法向量o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));//得到世界坐标系下的真正法向量(而非凹凸贴图产生的法向量)和雪落//下相反方向的点乘结果,即两者余弦值,并和_Snow(积雪程度)比较if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow))//此处我们可以看出_Snow参数只是一个插值项,当上述夹角余弦值大于//lerp(1,-1,_Snow)=1-2*_Snow时,即表示此处积雪覆盖,所以此值越大,//积雪程度程度越大。此时给覆盖积雪的区域填充雪的颜色o.Albedo = _SnowColor.rgb;else//否则使用物体原先颜色,表示未覆盖积雪 o.Albedo = c.rgb;o.Alpha = 1;
}ENDCG} FallBack "Diffuse"
}

使岩石随积雪厚度变形

最后一步是将岩石模型沿着下雪的方向变厚,以表示积雪的厚度。

为了达到此效果,我们需要修改模型的顶点数据 - 意味着我们要在表面着色器中写一个修改顶点的函数。

#pragma surface surf Lambert vertex:vert

我们在上述代码的末尾加入了vertex:vert表示我们将使用自己定义的vertex函数,此函数名称为vert。

我们的vertex函数如下:

void vert (inout appdata_full v) {//将_SnowDirection转化到模型的局部坐标系下float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3)){v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;}}

首先我们传给vert函数一个参数appdata_full v,参数的类型为appdata_full(Unity内置类型),该类型包含了纹理坐标,法向量,顶点位置,以及切线信息。如果你还需要使用其他的数据类型,你可以使用自定义的输入结构体作为pixel函数的第二个参数传递额外的信息 — 目前我们不需要这样做。

_SnowDirection使用的是世界坐标系,但是我们需要的其实是模型局部坐标系下的_SnowDirection。所以我们需要先将_SnowDirection转化到模型的局部坐标系下。而我们只需要将_SnowDirection乘以Unity内置矩阵 – UNITY_MATRIX_IT_MV(IT表示Inverse Transpose逆转置矩阵,MV表示 ModelView矩阵,该矩阵表示是ModelView的逆转置矩阵)。

现在我们得到了该顶点的法向量(vert函数应该是对每个vertex调用一次,相对于surf函数对每个pixel调用一次)。我们仍然像上面做积雪效果时那样,将转换坐标系空间后的雪落下相反方向和模型局部坐标系下的法向量进行点乘,得到的结果仍然和一个插值比较。不过此时插值项不再是_Snow,而是_Snow*2/3,这表示只有那些更接近雪落下方向的区域才会增加雪的厚度,更符合自然现象。

而这些通过测试的区域,沿着(sn.xyz+v.normal)方向进行加厚,也就是将其顶点沿此方向伸展一定距离。注意到增厚的程度取决于_SnowDepth和_Snow,而增厚的方向是由物体法向和雪落的方向综合作用的,这也符合自然现象。

我们来对比下不同的积雪深度效果:

我们的shader终于完成了。

源代码

完整的shader源码呈现如下:

Shader "Custom/SnowShader2" {Properties {_MainTex ("Base (RGB)", 2D) = "white" {}_Bump ("Bump", 2D) = "bump" {}_Snow ("Snow Level", Range(0,1)) = 0_SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)_SnowDirection ("Snow Direction", Vector) = (0,1,0)_SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1}SubShader {Tags { "RenderType"="Opaque" }LOD 200CGPROGRAM#pragma surface surf Lambert vertex:vertsampler2D _MainTex;sampler2D _Bump;float _Snow;float4 _SnowColor;float4 _SnowDirection;float _SnowDepth;struct Input {float2 uv_MainTex;float2 uv_Bump;float3 worldNormal;INTERNAL_DATA};void surf (Input IN, inout SurfaceOutput o) {//该像素的真实颜色值half4 c = tex2D (_MainTex, IN.uv_MainTex);//从凹凸贴图中得到该像素的法向量o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));//得到世界坐标系下的真正法向量(而非凹凸贴图产生的法向量)和雪落//下相反方向的点乘结果,即两者余弦值,并和_Snow(积雪程度)比较if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow))//此处我们可以看出_Snow参数只是一个插值项,当上述夹角余弦值大于//lerp(1,-1,_Snow)=1-2*_Snow时,即表示此处积雪覆盖,所以此值越大,//积雪程度程度越大。此时给覆盖积雪的区域填充雪的颜色o.Albedo = _SnowColor.rgb;else//否则使用物体原先颜色,表示未覆盖积雪 o.Albedo = c.rgb;o.Alpha = 1;}void vert (inout appdata_full v) {//将_SnowDirection转化到模型的局部坐标系下float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3)){v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;}}ENDCG} FallBack "Diffuse"
}

所有的资源代码我都放到这里了。

最后来张血雨效果图…

Unity3D Shader 新手教程(2/6) —— 积雪Shader相关推荐

  1. 【游戏渲染】【译】Unity3D Shader 新手教程(1/6)

    http://gad.qq.com/article/detail/7175490 该文章来自用户转载 点击阅读原文 刚开始接触Unity3D Shader编程时,你会发现有关shader的文档相当散, ...

  2. Unity3D Shader 新手教程(1/6)

    刚开始接触Unity3D Shader编程时,你会发现有关shader的文档相当散,这也造成初学者对Unity3D Shader编程望而却步.该系列教程的第一篇文章(译者注:即本文,后续还有5篇文章) ...

  3. 【译】Unity3D Shader 新手教程(1/6)

    刚开始接触Unity3D Shader编程时,你会发现有关shader的文档相当散,这也造成初学者对Unity3D Shader编程望而却步.该系列教程的第一篇文章(译者注:即本文,后续还有5篇文章) ...

  4. 为新手准备的 Codea 着色器(Shader)教程

    为新手准备的 Codea 着色器(Shader) 教程 原文标题:<Shaders for dummies>  作者:Ignatz  译者:FreeBlues  译文链接:http://m ...

  5. Unity3D Shader官方教程翻译(三)

    Unity3D Shader官方教程翻译(三) 1.Shader语法:Pass 1个Pass块可以使一个几何物体被一次渲染. Pass { [Name and Tags] [RenderSetup] ...

  6. 【转载】Unity3D导入图片属性信息和默认shader介绍

    本篇博文转载自:https://www.jianshu.com/p/e2aa4d898fb7 作者:shimmery 图片 Unity3D支持使用大部分位图格式作为图片素材,甚至包括带图层和图层效果的 ...

  7. Unity初学者Shader Graph教程

    Unity初学者Shader Graph教程 了解面向非程序员的 Unity 引擎可视化着色器编程工具的来龙去脉 课程英文名:Your Ultimate Guide to Shader Graph f ...

  8. threejs(webgl)-shader入门教程(1)

    1.shader基本使用 2.shader粒子 shader是什么? shader是一个用GLSL编写的小程序,也就是着色器语言,我们可以通过shader来编写顶点着色器和片元着色器,在WEBGL编程 ...

  9. Shader入门教程

    学习Unity有一段时间了,都说Unity想要进阶得学会Shader编程,因此花了一阵子来学习Shader编程.学了之后才发现,Shader并没有我原先想的那么复杂,掌握它的原理和语法后,我们也能用s ...

  10. Unity3d基础知识之Texture纹理、Shader着色器、Material材质、Rendering Mode

    Unity3d基础知识之Texture纹理.Shader着色器.Material材质.Rendering Mode 一.纹理.着色器与材质 Texture(纹理):应用于网格表面上的标准位图图像.Un ...

最新文章

  1. sqlserver去掉密码强度验证_安装sql2008数据库引擎配置时,提示sa密码强度不满足要求怎么办?...
  2. 音效摸鱼还不够爽?试试IDE里打几盘魂斗罗?
  3. 双边滤波算法的原理、流程、实现及效果
  4. JS中获取焦点和选中的元素
  5. TypeForwardedTo Attribute ---- 类型传递
  6. LINQ to Entities 不支持 LINQ 表达式节点类型“ArrayIndex”
  7. Varnish——CDN加速实现(单个后端服务器、缓存命中率情况)
  8. “木马源”攻击影响多数编程语言的编译器,将在软件供应链攻击中发挥巨大作用...
  9. UVA10849 Move the bishop【国际象棋】
  10. csgo跳投绑定指令_csgo控制台指令大全
  11. 拓端tecdat|R语言中进行期权定价的Heston模型
  12. 数据库索引是什么,它的作用是什么?
  13. 产品经理入门知识梳理
  14. 毕业论文学术报告答辩开题报告PPT模板
  15. PHP连接MySQL数据库的三种方式
  16. 随笔-人生第一份工作离职了
  17. 计算机工作理想湿度,计算机理想的工作温度七月的盛夏,碧空中没有一丝云彩,只剩下纯(9)...
  18. win10 桌面(Windows 资源管理器)卡死的根本解决办法
  19. 什么是独立站,独立站的作用是什么?
  20. 数据挖掘 第四篇:OLS回归分析

热门文章

  1. 精密测量仪器的气源维护知识
  2. IT:如何把骨干留住
  3. gis生成道路中心线_ArcGIS方法-利用到路面提取道路中心线的方法
  4. 联想官方OEM分区制作
  5. Python模拟银行管理系统(面向对象)# 谭子
  6. 百度云破解不限速版(绿色免安装)
  7. QTTabBar安装与使用: 更胜浏览器的Windows平台浏览文件方式
  8. 四大名著红楼梦第二回 贾夫人仙逝扬州城 冷子兴演说荣国府
  9. 电子加速器原理与应用
  10. 提交form表单之前处理数据