创建一个Toon风格的着色器

创建一个Toon风格的着色器 Toon着色器(toon shading) 是游戏中最常使用的效果之一,也被称作(AKA) cel shading (cel是celluloid的缩写[中文也叫 赛璐珞])。它是一种非真实渲染技术,可以让3D模型呈现一种平面效果。许多游戏中用这种着色器把3D的物体渲染成一种手绘物体的效果。下图中你能看到这两者的区别,右边的是标准着色器,左边的是Toon着色器: 如果只使用 表面函数(surface functions) 虽然也能获得这样的效果,但是花费性能和时间的代价太大了。表面函数仅仅对材质的属性起作用,而对材质的具体光照环境无能为力。因为toon着色器需要改变光的反射方式,所以我们接下来要创建我们自己的光照模型。 始前准备 开始学习这个知识点之前,我们先创建一个着色器和对应的材质球,而且需要导入一个特殊的纹理,步骤如下: 创建一个新的着色器;在这例子中,我们会用上一个知识点的着色器进行扩展 为着色器创建一个新的材质,并且把它应用到3D模型中。 拥有曲面的模型对于toon着色器来说最好。 这个知识点需要一张额外的纹理,叫做 ramp 贴图[如果要全译,我把它叫梯度贴图]。有一点很重要,在导入的时候把 Wrap Mode 改为 Clamp ,如果你想让颜色的边缘变得灵敏,就把 Filter Mode 设置为 Point : 操作步骤 通过下面的步骤我们可以获得toon风格的特殊审美呈现: 添加一个新的叫 _RampTex 纹理属性: _RampTex ("Ramp", 2D) = "white" {} 同时在 CGPROGRAM 块中添加对应的变量: sampler2D _RampTex; 修改 #pragma 指示 ,从而让着色器使用一个叫 LightingToon() 的函数: #pragma surface surf Toon 使用下面这个光照模型: fixed4 LightingToon(SurfaceOutput s ,fixed3 lightDir,fixed atten) { half NdotL = dot(s.Normal,lightDir); NdotL = tex2D(_RampTex,fixed2(NdotL,0.5)); fixed4 c; c.rgb = s.Albedo * _LightColor0.rgb*NdotL*atten; c.a = s.Alpha; return c; } 原理介绍 toon风格着色器的主要特征是它的光的渲染方式;表面并非均匀的着色。为了能达这种效果,我们需要一张 ramp 贴图。他的作用是对 Lambertian 的光线强度 NdotL 重新映射,然后把值赋值给另一个值。我们使用一张 ramp 贴图而不是一个梯度值,是为了强制光线按照步骤渲染。下图展示了 ramp 贴图是如何纠正光的强度的: ...

February 23, 2021 · 1 min · 149 words · Link

理解光照模型

第三章 理解光照模型 在前面的那些章节中,我们介绍了表面着色器并且还理解了如何修改一些物理属性(比如 Albedo 和 Specular )来模拟不同的材质。这些到底是如何工作的呢?每个表面着色器中最重要的部分一一 光照模型lighting model 。它的功能就是接受这些参数然后计算每一个像素点的最终着色。Unity通常会对开发者隐藏这部分,因为如果想要编写一个光照模型的话,你就必须要去理解光在物体表面是如何反射和折射的。这个章节中我们会毫无保留的向你展示光照模型是如何工作的,并且给你介绍一些你自己创建光照模型所需要的一些基础知识。 这一章中,我们会学习下面所列的知识点: 创建一个自定义的漫反射光照模型 创建一个Toon风格的着色器 创建一个Phong类型类型的高光反射着色器 创建 BlinnPhong 类型的高光反射着色器 创建各向异性类型的高光反射着色器 介绍 想要模拟光的工作方式是一项非常具有挑战性的工作,同时也非常消耗计算资源。在之前的很早一段时间内,游戏中使用的都是一些非常简单的光照模型,效果看起来差到难以置信。尽管现在的3D游戏引擎已经使用了基于物理原理的渲染器,但是有些更简单的光照模型技术还是值得我们去探索的。但有时,我们不得不面对资源紧张的现实,没有办法在这些资源有限的设备上完整实现光照模型,比如我们的移动设备。所以你想在这上面实现自己的光照模型,那么你就很有必要了解这些简单的光照模型。 创建一个自定义的漫反射光照模型 如果你对Unity4很了解的话,你应该知道Unity提供的默认的着色器是基于一个叫 Lambertian reflectance 的光照模型。我们会在这个知识点向你展示如何创建一个自定义的光照模型,并且解释它后面的数学原理和实现方式。下面的两张图分别展示了 标准着色器Standard Shader (右) 和 diffuse Lambert 着色器对同一个几何体进行渲染后,不同的显示效果: 基于 Lambertian reflectance 光照模型的着色器是典型的非真实渲染着色器;我们现实生活中没有物体会看起来像那样。然而Lambert 着色器依然能在一些低多边形风格的游戏中经常看到,因为跟一些复杂的几何体比起来,它们的三角面数量对比非常明显。用于计算 Lambertian 反射的光照模型非常高效,这特别适合移动端的游戏。 Unity其实已经提供了光照函数给我们,好让我们能在着色器中使用。它就是 Lambertian 光照模型。它是光反射模拟的一种更基础更有效率的形式,你能在当今的很多游戏中看到它的存在。 因为它们已经内建在了Unity的表面着色器语言中,所以我从这个开始和基于它开始构建也不失为一个好的选择。你也可以在Unity用户手册中找到一个例子,但我们还是会更深入学习,从而向你解释这些数据是从哪里来的以及为什么它是那样工作的。这些可以为设置光照模型打下一个很好的基础,这些知识将来也能在后面的章节中对我们有帮助。 始前准备 让我们从实现下面几个步骤开始: 创建一个新的着色器并且给它命名好。 创建一个新的材质球,命名好,并且把上一步新建的着色器应用于该材质。 接下来,创建一个球形对象,并且把它大致放在场景中间的位置。 最后,我们创建一个方向光源,让光找到游戏对象上。 当你在Unity中设置好这些资源后, 你就会有一个类似于下图的场景: 操作步骤 Lambertian 反射可以在着色器中修改下面的代码实现: 首先在着色器的 属性Properties 块中添加下面的属性: _MainTex("Texture", 2D) = "white" 修改着色器的 #pragma 指示符,从而让着色器使用我们自定义的光照模型,而不是 标准Standard : #pragma surface surf SimpleLambert 使用一个非常简单的 表面函数surface function ,这个函数仅仅通过它的UV数据对纹理进行采样: void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D(_MainTex,IN.uv_MainTex).rgb; } 添加一个叫做 LightingSimpleLambert() 的函数,这个函数包含了下面实现 Lambertian 反射的代码: half4 LightingSimpleLambert(SurfaceOutput s,half3 lightDir,half atten) { half NdotL = dot(s.Normal,lightDir); half4 c; c.rgb = s.Albedo * _LightColor0.rgb * (NdotL*atten*1); c.a = s.Alpha; return c; } 原理介绍 ...

February 22, 2021 · 2 min · 223 words · Link

在地形的表面绘制一个圆

在地形的表面绘制一个圆 在很多的RTS类型的游戏中,通过围绕被选中的单位绘制一个圆圈来表示距离(攻击范围,可移动距离,视野等等)。如果地面是平坦的,那么可以用通过对一张绘制有圆圈的矩形纹理进行缩放就能简单的做到。但是如果地面并不平坦,那么这个矩形的纹理就很有可能被高出的山丘或者其他几何物体遮住。接下来的知识点将展示如何编写一个着色器,让你可以在任何复杂的物体表面绘制一个圆圈[且不会被遮住]。如果你想对这个圆圈移动或者执行动画,那么我们就需要有着色器和C#代码才行。下图展示了用着色器在一个丘陵地形中绘制一个圆圈的例子: 始前准备 这里的着色器主要是用于地形的,对于其他的游戏对象不适用。所以我们首先要做的是在Unity中创建好一个地形。 我们先分别建立一个着色器和材质,名字分别是 RadiusShader 和 RadiusMaterial 。 当你的角色物体准备好后;我们会绕着它绘制一个圆圈。 在Unity的菜单[这里的菜单操作我就不翻译了,翻译了感觉反而不好,怕翻译错了,大家找不到],选择 GameObject | 3D Object | Terrain 来创建一个新的地形。 为地形创建一些几何面。你可以导入一个已存在的地形或者自己用地形工具画一个新的。(Raise/Lower Terrain, Paint Height, SmoothHeight )[括号里面这些都是Unity地形编辑器中的工具] 地形是Unity中特殊的游戏对象,它表面的纹理映射方式跟传统的3D模型不一样。你不能通过在着色器定义一个 _MainTex 来提供纹理,因为在地形中需要直接由地形自己提供。这一步骤可以通过在地形编辑器中选择 Paint Texture 然后点击 Add Texture…: 完成上面步骤后,纹理就设置好了。你必须修改地形的材质这样就可以在地形中使用我们提供的自定义的着色器。在 Terrain Settings 中, 把 Material 一栏的属性改为Custom ,接着把我们的 Radius material 材质拖拽到 Custom Material 栏上。 接下来就要准备你自己的着色器了。 操作步骤 让我们开始编辑着色器RadiusShader 的代码: 在新的着色器中, 添加下面四个属性: _Center("Center", Vector) = (0,0,0,0) _Radius("Radius", Float) = 0.5 _RadiusColor("Radius Color", Color) = (1,0,0,1) _RadiusWidth("Radius Width", Float) = 2 然后在CGPROGRAM块中添加它们各自的变量与之对应: float3 _Center; float _Radius; fixed4 _RadiusColor; float _RadiusWidth; 所以现在输入表面函数的数据不仅仅是纹理的UV数据了,还包括地形的中每一个点的位置(这个位置是基于世界坐标的)。 为了拿到这个参数我们需要修改输入结构体 Input struct,如下所示: struct Input { float2 uv_MainTex; // The UV of the terrain texture float3 worldPos; // The in-world position }; 最后我们在表面函数中使用这个参数: void surf(Input IN, inout SurfaceOutputStandard o) { float d = distance(_Center, IN.worldPos); if (d > _Radius && d < _Radius + _RadiusWidth) o.Albedo = _RadiusColor; else o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb; } 通过上面的步骤,你就可以在地形中绘制一个圆。你可以通过材质的检查器面板Inspector tab去改变这个圆的位置,半径和颜色。 ...

January 16, 2021 · 1 min · 200 words · Link

纹理的压缩和混合

纹理的压缩和混合 纹理的作用并不仅仅只是我们通常认为的保存加载的数据或者像素颜色,同时还有像素点在x和y方向以及RGBA通道的各种设置。我们能把多张图片压缩进一张单独的RGBA纹理中并且使用它们各自的R,G,B和A元素,因为我们可以在着色器中把它们各自纹理中的这些元素分别解压出来。 将各自的灰度图压缩进一张单独的RGBA纹理的结果可以通过下图看出来: 为什么说这会有用呢?在你的应用程序实际消耗的大部分中内存当中,贴图占了很大的一部分。所以如果你想要减少应用程序的大小的话,我们能在着色器中查看所有使用的图片并且想想我们是否能将这些纹理合并到一张单独的纹理中。 任何灰度的纹理都可以压缩进另一张有RGBA通道的纹理。第一次听起来可能有点怪怪的,但我们接下来会用这个知识点来演示纹理压缩的用法并且在我们的着色器中使用这张压缩过的纹理。 举其中一个纹理压缩用法例子,比如你想把一套纹理[有好几张]混合进一张单独的纹理中。这在地形类着色器中很常见,在我们的例子中,我们会用一些排好序的控制纹理或者压缩过的纹理,很好的混合进另一张纹理中。这个知识点会讲到这个技术的,同时还会告诉你如何开始编写好这样一个混合四张纹理的着色器。 始前准备 在我们Unity的着色器文件夹中创建一个新的着色器同时创建一个新的材质与之对应。这两者的命名怎么方便怎么来,不过尽量保证组织和引用方便吧。 建好着色器和材质后,再创建一个新的场景,好给后面做测试。 收集好四张你打算混合在一起的纹理。我们直接用它们展示这几张纹理是如何放到物体表面的。 我们可以用一些非常复杂的混合纹理在地形网格上创建一个非常真实的地形分布纹理,如下所示: 操作步骤 我们通过下面代码来学习如何使用压缩纹理。 我们在着色器的 属性Properties 块中添加一些属性。我们需要5个 sampler2D 类型的游戏对象或纹理,2个颜色属性: Properties {_ MainTint ("Diffuse Tint", Color) = (1,1,1,1) //Add the properties below so we can input all of our textures _ColorA ("Terrain Color A", Color) = (1,1,1,1) _ColorB ("Terrain Color B", Color) = (1,1,1,1) _RTexture ("Red Channel Texture", 2D) = ""{} _GTexture ("Green Channel Texture", 2D) = ""{} _BTexture ("Blue Channel Texture", 2D) = ""{} _ATexture ("Alpha Channel Texture", 2D) = ""{} _BlendTex ("Blend Texture", 2D) = ""{} } 接下来我们在 SubShader{} 块中创建一些变量,记住要跟上一步的属性块对应。 CGPROGRAM #pragma surface surf Lambert float4 _MainTint; float4 _ColorA; float4 _ColorB; sampler2D _RTexture; sampler2D _GTexture; sampler2D _BTexture; sampler2D _BlendTex; sampler2D _ATexture; 我们现在获得了纹理属性后把它们传递给 SubShader{} 函数。为了能够让使用者可以控制每个纹理的截取比例,我们需要修改 输入结构体Input struct 。这样我们就可以使用每个纹理的截取和偏移量等参数: ...

January 14, 2021 · 2 min · 332 words · Link

创建一个有全息效果的着色器

创建一个有全息效果的着色器 近些年来太空主题的发行的越来越多。科幻游戏中很重要的一个部分就是在游戏中集合了来自未来的各种技术。全息投影就是其中的典型代表。全息投影尽管有很多种形式,但是通常用一种半透明,看起来很薄的投影来呈现。这次的这个知识点将会向你展示如何创建一个这样的着色器来模拟这样的效果。我们首先想到:要创建一个优秀的全息投影特效,你需要能够添加噪音,扫描动画和和震动。下图就展示了一个全息投影效果的列子: 始前准备 正如全息投影效果展示的知识物体的轮廓,所以我们可以把我们的这个着色器命名成Silhouette[轮廓的意思]。把它跟材质关联起来并且把它应用到你的3D模型中去。 操作步骤 根据下面的步骤可以将我们的当前的着色器修改为有全息投影效果的着色器: 在着色器中添加下面的属性: _DotProduct("Rim effect", Range(-1,1)) = 0.25 并且添加跟属性对应的变量到CGPROGRAM块中去: float _DotProduct; 因为这个材质是有透明度的,所以需要添加下面的标签: Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" } 注意 根据你将会使用的游戏对象类型,你可能想要它的背面也能看到。如果是这种情况,那么我们就需要在代码中添加Cull Off,从而让模型的背面不会被剔除。 这个着色器并不会尝试去模拟真实世界的材质,所以这里就没有必要再使用PBR关照模型了。我们将会用性能消耗更少的Lambertian 反射来代替它。另外,我们应该使用nolighting来关闭所有的光线并且用alpha:fade来告诉Cg我们得着色器是一个有透明度的着色器: #pragma surface surf Lambert alpha:fade nolighting 修改输入结构体从而能让Unity输入当前的视口方向和世界的法线方向: struct Input { float2 uv_MainTex; float3 worldNormal; float3 viewDir; }; 修改你的表面函数surface function成下面的样子。请记住因为这个着色器使用Lambertian反射作为光照函数,所以表面输出结构体的名字也要相应改成SurfaeOutput,这是SurfaceOutputStandard类型的实例。 void surf(Input IN, inout SurfaceOutput o) { float4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; float border = 1 - (abs(dot(IN.viewDir, IN.worldNormal))); float alpha = (border * (1 - _DotProduct) + _DotProduct); o.Alpha = c.a * alpha; } 现在你可以使用Rim effect这个滑动条来选择全息投影效果的强度。 原理介绍 正如前面提到的,这个着色器仅仅是展示了物体的轮廓。如果我们从不同的角度看这个物体,它的轮廓也会改变。从几何学的角度上讲,模型的所有边都包含在法线方向normal direction垂直于视口方向view direction的三角形上。输入结构体Input structure声明了这些变量,分别是worldNormal和viewDir这两个参数。 要知道两个向量是否垂直可以用点积dot product进行判断。她是一个操作符,接收两个向量作为参数,如果这两个向量垂直,会返回零。我们使用参数DotProduct来控制点积趋近于零的程度从而控制那些三角形应该完全消失。 这个着色器的另一方面,我们用了**_DotProduct(不可见)**来确定模型的边(完全可见)和角度之间消失的力度。这个线性插值是通过下面的代码实现的: float alpha = (border * (1 - _DotProduct) + _DotProduct); 最后,贴图原来的alpha值乘以一个计算好的系数后,我们获得了最终的样子。 额外内容 这种技术非常的简单并且性能消耗相对较低。不过这种着色器还可以用于其他的各种各样的特效,比如下面的这些: 科幻游戏中包裹星球的浅色大气层 被选中的游戏物体的边或者当前鼠标下的物体 鬼魂或者幽灵 引擎冒出的烟 爆炸的冲击波 太空战舰被攻击时的防护罩 相关补充 在反射计算中向量的点积dot product扮演着非常重要的角色。我们在 第三章,理解光照模型 这个章节中会详细的介绍它是如何工作的以及为什么会广泛的用于很多的着色器中。 ...

January 13, 2021 · 1 min · 126 words · Link

创建一个带透明度的材质

创建一个带透明度的材质 到目前为止我们所看到的着色器似乎都有一个共同点——它们都用于了固体材质。如果你想要的提升你的游戏的视觉效果,那么带透明度的材质通常是一个好的开始。它们用途广泛,从火焰效果到窗户的玻璃都会用到它们。但稍微麻烦的是,它们用起来要复杂一点点。在渲染固体模型之前,Unity会根据它们离摄像机的距离(这个叫Z ordering)进行排序,并且跳过渲染所有背朝摄像机的三角面(这个是剔除技术culling)。当渲染带透明度的几何物体时,这些含有两个面的几何物体就会产生问题。这次的知识点将会为你展示:当我们创建有透明度的表面着色器时,我们如何解决这个过程中产生的这些问题。我们在**第六章,片元着色器和抓取通道**我们还会着重回顾这个话题,真实渲染中的玻璃和水体着色器都会涉及到。 始前准备 这个知识点需要一个新的着色器,我们就叫它Transparent吧。同时也需要一个新的材质,这样才能把着色器应用于游戏物体。因为这个物体需要成为一个透明的玻璃窗,那么我们最好用Unity中的quad或者plane来做。我们当然也需要一些不透明的其他游戏对象来对比测试效果。在这个例子中,我们会使用一张PNG图片作为玻璃纹理。这张图片的alpha通道将会用于控制玻璃的透明度。这样的PNG图片大家自行自作,软件不限。但是需要遵守下面的这些步骤: 找一张要用于你窗户玻璃的图片。 用照片编辑软件打开这样图片,比如GIMP或者Photoshop。 选择图片中你想要变成半透明的部分。 在这张图片上创建一个空白(full opacity[抱歉我不懂PS,不知道这个参数的含义])图层 选择上一步创建的图片,以黑色来填充这个图层。 保存图片然后导入到Unity中。 这个知识点中,我们用来试验的图片是一张来自圣斯德望主教座堂 ***Meaux Cathedral in France(https://en.wikipedia.org/wiki/Stained_glass)***的花窗玻璃。 如果遵循了上面的图片制作步骤,那么你也会获得如下类似的一张图片(RGB通道在左图,A通道在右图): 操作步骤 正如我们前面提醒的,我们在使用透明度着色器时需要注意几个方面: 在着色器的SubShader{}代码块中,添加下面的标签告知着色器这是用于透明度的: Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" } 由于这个着色器是为2D 材料设计的,所以确保你的模型的背面不会被绘制,通过添加下面的这个代码: Cull Back 告诉着色器这个材料是半透明的并且需要跟屏幕中绘制的什么内容混合: #pragma surface surf Standard alpha:fade 使用这个表面着色器来决定玻璃的颜色和透明度: void surf(Input IN, inout SurfaceOutputStandard o) { float4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Alpha = c.a; } 原理介绍 这个着色器中介绍了几个新的概念。首先,标签Tags用于添加一些关于游戏对象是如何渲染的信息。但是这次这里真正让人感兴趣的是Queue。Unity默认会根据物体距离摄像机的距离来对游戏对象进行排序。所以当一个物体距离摄像机越近,那么它就会比所有离摄像机更远的物体先绘制。在大多数情况下,它对游戏来说都没有问题,但是有些情况下你可能想要自己控制游戏场景中物体的渲染顺序。Unity已经有提供给我们一些默认的渲染队列,每一个队列都有一个单独的值好让Unity按照这个顺序在屏幕中绘制游戏物体。这些内建的渲染队列分别叫做**[这些参数就不翻译了]** Background,Geometry,AlphaTest,Transparent, 和 Overlay。 这些队列并不是随意创建的;创造它们是为了让我们能更容易的编写着色器代码和跟实时渲染进行交互。下表描述了每一个不同的渲染队列的作用: 渲染队列 队列描述 渲染队列值 Background 这个渲染队列最先渲染。它被用于天空盒等等。 1000 Geometry 这个是默认的渲染队列。大多数游戏对象都用这个 。不透明物体使用这个队列。 2000 AlphaTest Alpha-tested几何体使用这个队列。跟 Geometry队列不同的是在所有的固态物体都被渲染的情况下,它在渲染alpha-tested游戏对象上效率更高。 2450 Transparent 这个队列在Geometry队列和AlphaTest队列之后,并且是由后向前渲染顺序。任何的alpha渲染(着色器不会写入深度buffer)都应该在这个队列,比如玻璃效果和例子效果。 3000 Overlay 这个渲染队列是为叠加效果准备的。所有在最后渲染的东西都用此队列,比如镜头光晕。 4000 所以,一旦你知道你的游戏对象属于哪个渲染队列后,那么你就可以给它设置渲染队列。我们这次的着色器用到了Transparent队列,所以我们在代码中用到了**Tags{“Queue”=“Trasparent”}.**这样的标签。 ...

January 12, 2021 · 1 min · 98 words · Link

法线贴图

法线贴图 3D模型的每一个三角面都有一个朝向facing direction,这个朝向就是它向前的指向。它通常用一个垂直并且放置在三角面正中心的一个箭头来表示。这个朝向对于光在表面反射中来说非常的重要。如果相邻的两个三角面朝向不同的方向,那么它们对光的反射也会朝向不同的角度,也就是在着色器中它们的处理方式会不一样。对于曲面物体来说,这里有个疑问:很显然这些拥有曲面的几何体仍然是由平面三角形构成的,那光线改如何处理? 为了避免这个问题,对应三角面的光的反射计算方式此时不根据它的朝向计算,而是根据它的法线方向normal direction方向计算。前面向着色器添加纹理那个知识点讲到,顶点是保存有数据的。法线方向信息也是继UV信息之后,保存在顶点中最有用的信息。这是一个单位长度的向量,并且它表示了顶点的朝向。不考虑朝向的话,那么三角面内的每一个顶点都有它自己的法线方向,只不过这个法线方向是一个存储在顶点中的线性插值。这给了我们用低模模拟高模的能力。下面示例图展示了同一个几何形状在不同的顶点插值密度下的表现。在左边,法线垂直于由顶点表示的面;很明显每个面之间有明显的的割裂感。再看看最右边的几何体,它的法线是通过他的面线性插值得到的,可以看出来的是,尽管它的表面看起来还是很粗糙,但是光线的反射看起来却似乎很光滑。很容易看出来尽管这三个物体的几何体都相同,但是它们的光线反射却不一样。尽管都是由平面三角形构成,但是右边物体的光线反射似乎看起来像曲面反射。 一个有着粗糙的边的光滑物体很明显的表示单位顶点的法线肯定进行了线性插值。如果我们对保存在每个顶点的法线按其方向进行绘制,我们就能够看到它们,正如下图所展示的那样。你应该注意的是每个三角形仅有三条法线,但是相连的三角形有相同的顶点,会看到不止有一条法线从中绘制出来。 法线贴图在计算3D模型的法线技术中脱颖而出。跟纹理贴图类似,法线方向也可以用一张额外的纹理表示,我们把它们叫做法线贴图normal map或者凹凸贴图bump map。 法线贴图通常是一张RGB图片,里面的RGB通常分别用来表示法线方向中的X,Y,Z方向。现在有很多种技术方法来创建一张法线贴图。比如这些应用程序,CrazyBump(http://www.crazybump.com/)跟NDO Painter(http://www.crazybump.com/)可以把2D数据转换成法线数据。其他的应用程序比如Zbrush 4R7(http://www.pixologic.com/)和AUTODESK(http://usa.autodesk.com)可以把雕刻数据转换成法线贴图。如何创建法线贴图完全超出了本书的范畴,但上面的内容对你了解相关相应的知识还是有好处的。 在Unity中向着色器添加法线的过程很简单,因为在表面着色器中有着**UnpackNormals()**这样的方法给你调用。就让我们看看这是怎样的一个过程。 始前准备 分别创建一个新的材质和着色器,并且把它设置到场景视图Scene view中的游戏对象中去。这样的话,我们的项目非常简单,好让我们仅仅是观察法线贴图这项技术。 这个知识点中你需要一张法线贴图,但是我们这本书附带的Unity工程中包含了一张。[当然,你也可以从我这里把这张图片下载下来,如下图] 操作步骤 下面就是创建法线贴图着色器的步骤了: 让我们设置好我们的属性块,从而可以获得颜色和贴图: Properties { _MainTint ("Diffuse Tint", Color) = (1,1,1,1) _NormalTex ("Normal Map", 2D) = "bump" {} } 注意 因为用的是bump来初始化了属性的贴图类型,这等于是告诉了Unity**_NormalTex包含了法线贴图。如果这个贴图没有被设置, 那么会默认给它设置一张灰色的贴图。颜色值会用(0.5,0.5,0.5,1)**,然后看不出一点凹凸感。 在CGPROGRAM下面的**SubShader{}**块中声明下面两个变量,让这两个变量跟属性块中的两个属性关联起来: CPROGRAM #pragma surface surf Lambert // Link the property to the CG program sampler2D _NormalTex; float4 _MainTint; 我们需要修改输入结构体Input struct的名字,从而让我们可以让我们通过模型的UV来访问法线贴图: // Make sure you get the UVs for the texture in the struct struct Input { float2 uv_NormalTex; } 最后,我们通过内建的**UnpackNormal()**函数从法线贴图中提取出我们需要的法线信息。接着,你只要把这些新的法线应用到表面着色器的输出上即可: // Get the normal data out of the normal map texture // using the UnpackNormal function float3 normalMap = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex)); // Apply the new normal to the lighting model o.Normal = normalMap.rgb; 下图展示了我们的法线贴图着色器的最终效果: ...

January 9, 2021 · 1 min · 149 words · Link

通过改变UV值来移动纹理

通过改变UV值来移动纹理 在当惊的游戏产业中,一个很常见的游戏纹理技术就是允许你对游戏物体表面的纹理进行滚动。这种技术可以让你创建很多效果,比如瀑布,河流,流动的沿江等等。同时这些技术也是制作动画精灵特效的基础,我们会在这一章节的一系列知识点中来讲解这些内容。 首先,让我们来看看在**表面着色器(Surface Shader)**如何创建一个简单的纹理滚动效果。 始前准备 在这个知识点开始之前,需要你创建一个新的着色器文件和材质。这么做的目的是为了有个干净的着色器,然后我们可以更加方便的学习和观看滚动效果。 操作步骤 闲话少说,我们打开刚才创建的的着色器[着色器的名字文章中没有说,就自己取一个启动的名字吧],然后输入下面每个步骤所展示的代码: 这个着色器需要两个控制纹理滚动的新属性。所以我们添加一个速度属性控制X方向的滚动,添加另一个速度属性控制Y方向的滚动,如下面的代码所示: Properties { _MainTint("Diffuse Tint",Color) = (1,1,1,1) _MainTex ("Base (RGB)", 2D) = "white" {} _ScrollXSpeed("X Scroll Speed",Range(0,10)) = 2; _ScrollYSpeed("Y Scroll Speed",Range(0,10)) = 2; } 修改CGPROGRAM代码块中的Cg属性中的变量,创建新的变量[把原来的都删掉,用下面展示的代替],这样我们就能访问来自着色器属性的值了: fixed4 _MainTint; fixed _ScrollXSpeed; fixed _ScrollYSpeed; sampler2D _MainTex; 修改表面函数surface function从而修改传递给tex2D()函数的UV值。然后,使用内建的_Time变量来对UV进行循环播放的动画,这样的话当我们点击Unity中的运行按钮的时候,我们就能看到动画效果了: void surf (Input IN, inout SurfaceOutputStandard o) { // Create a separate variable to store our UVs // before we pass them to the tex2D() function fixed2 scrollUV = IN.uv_MainTex; // Create variables that store the individual x and y // components for the UV's scaled by time fixed xScrollValue = _ScrollXSpeed * _Time; fixed yScrollValue = _ScrollYSpeed * _Time; // Apply the final UV offset scrollUV += fixed2(xScrollValue,yScrollValue); // Apply textures and tint half4 c = tex2D(_MainTex,scrollUV); o.Albedo = c.rgb * _MainTint; o.Alpha = c.a; } 下面的图片中的示例就是利用滚动UV的系统来创建的一个自然环境中河流的动画,你可以注意到场景中叫ScrollingUVs的特效就是来自于本书提供的代码: ...

January 9, 2021 · 1 min · 191 words · Link

在LayaBox中使用Unity中的导航网格,实现AI自动寻路

在LayaBox中使用Unity的导航网格,实现AI自动寻路 使用这个这个库的好处在于,你不必了解AStar算法,一样可以使用AStar算法来进行AI导航。只需要调用接口即可。 下面我给出LayaBox的示例项目地址和Unity导出网格示例项目地址,各位按需克隆下来即可 Unity示例项目:https://github.com/linkliu/ExportNavMesh LayaBox示例项目:https://github.com/linkliu/LayaNavMesh 原始的教程在http://ask.layabox.com/question/47899这里,大家可以去看看这里也行 这次的实例会从下面三个方面来讲解: Laya要用到的导航组件库NavMesh.js Unity如何将Navmesh数据导出成json文件【Laya中用到】 Unity中用到的NavMeshComponents 开始之前,说一下相关软件的版本 LayaAir 2.9 ,Laya引擎库2.7.1,Unity 2018.4.11f1 1.Laya中用到的导航组件库NavMesh.js 可以直接在Unity中对导航网格进行编辑,非常的方便。 NavMesh.js可以直接从这里去拿https://github.com/lear315/NevMesh.Js/tree/main/build 名字可能跟我的不一样,但是里面内容完全一样,我这里是强迫症发作,把Nev改成了Nav。然后只要拿NavMesh.js和NavMesh.d.ts这两文件就行了。NavMesh.js请放在Laya项目的bin/libs目录下面。NavMesh.d.ts放在项目的libs文件夹中。并且在bin/index.js中增加loadLib(“libs/NevMesh.js”),注意需在loadLib(“js/bundle.js”);前面。完成上面这些步骤,就把导航组件库NavMesh.js放到我们的项目中了 2.Unity如何将Navmesh数据导出成json文件 将Unity的导航网格数据导出成LayaBox需要的json数据,需要用到两个关键文件,一个是把导航网格转换成.obj文件的NavMeshExport.cs。另一个是Python自动转换脚本convert_obj_three.py,这两个文件的获取方式,我贴在下面: NavMeshExport.cs:https://github.com/lear315/NevMesh.Js/tree/main/unity convert_obj_three.py: https://github.com/lear315/NevMesh.Js/tree/main/python NavMeshExport.cs是一个Unity中的一个C#脚本,只要放到Unity中即可,便会在Unity中生成一个导出菜单,合并在LayaBox的导出菜单中。如下图 点击Export按钮,就会把当前的导航网格导出到ExportNavMesh文件中,里面就是需要下一步需要的.obj文件。 convert_obj_three.py是一个python脚本,所以各位需要安装python,并且配置配置好python环境,并且把python添加到系统的环境变量中去。 这个脚本的使用方法是 python convert_obj_three.py -i xx.obj -o xx.json,这个命令是把上一步生成的.obj文件转换成.json文件,这样我们就能在LayaBox中使用这个.json文件来进行AI导航了。 我的示例项目中已经做好了一键obj转json的功能,具体的用法是:选中你要转换的obj文件,然后右键,菜单选择Convert Navmesh to Json,就回自动在当前目录下生成一个同名的.json文件。这个就是LayaBox需要的文件,把这个文件放在LayaBox中的一个目录中。 3.Unity中用到的NavMeshComponents Unity中的导航网格的生成需要用到NavMeshComponents组件,目前这个组件Unity没有集成到Unity编辑器中,至少Unity2018以及之前的版本没有。但是Unity官方把它们放在Github上,地址在这里:https://github.com/Unity-Technologies/NavMeshComponents 克隆下来后,你只需要把Assets/NavMeshComponents这个文件复制到自己的项目中就行了,其他的东西可以不用。 NavMeshComponents的用法我就不细讲了,各位可以到https://docs.unity3d.com/Manual/NavMesh-BuildingComponents.html查看,也可以看这个中文的的博客https://blog.csdn.net/wangjiangrong/article/details/88823523各位按需观看吧。 总结 完成上面的三个步骤后,准备工作都OK了,具体的使用,各位可以去看我的LayaBox示例项目吧,哪里有完整的代码。 感谢各位耐心看完。

January 7, 2021 · 1 min · 45 words · Link

向着色器添加纹理

向着色器添加纹理 通过纹理,可以很容易让着色器变得生动起来,获得非常真实的效果。为了更有效的使用纹理,我们需要了解一张2D图片是如何映射到3D模型中去的。 这个映射的过程称之为纹理贴图texture mapping,为了完成映射,我们在使用的模型和着色器上还有额外的工作。模型实际上是由很多的三角形拼接而成的;而三角形的每个顶点都保存有着色器可以访问的各种数据。 其中很重要的一个信息就是UV信息 (UV data)。 它包含两个坐标,U和V,其取值范围是0到1。这两者表示2D图片的像素点坐标的XY位置信息,而这些信息将会映射到顶点中去。 UV数据只为顶点表示[意思可能也等价于:UV数据只存在顶点中]; 当三角内的点需要被纹理贴图时,GPU会插值最接近的UV值,从而从相应的纹理中找到正确的像素点。下面的图片展示了一张2D纹理贴图到3D模型中的三角形中的情况: UV数据保存在3D模型中并且需要3D模型工具去编辑它们。有些模型缺少UV组件,因而它们不支持纹理贴图。比如3D模型软件中的默认的那个兔子模型,就没有提供这么一个组件。 始前准备 学习这个知识点的时候,你需要一个有UV数据和纹理的3D模型。然后把它们都导入到Unity中。也可以直接拖拽到Unity编辑器中,会自动导入。因为标准着色器支持默认的纹理贴图。我们会用到这一点,而后会详细的介绍它是如何工作的。 操作步骤 用标准着色器给你的模型添加一张纹理异常的简单,按照下面步骤: 创建一个叫TexturedShader标准着色器。 创建一个名为TexturedMaterial的材质球。 通过拖拽的方式,把着色器赋值给材质,把着色器拖到材质上即可。 选择刚才的材质,然后拖拽模型对应的纹理到一个叫**Albedo(RGB)的矩形区域中的空白部分。如果你正确的执行了上述步骤,你的材质检查器面板(Inspector )**会如下图所示: 标准着色器知道如何通过UV信息把2D图像映射到3D模型中 原理介绍 当通过材质的检查器面板使用标准材质的时候,纹理贴图背后的处理过程对于开发者来说是透明的。如果我们想了解它是如何工作的,那我们需要更加详细的了解我们刚才创建的TexturedShader着色器。在着色器的属性Properties部分,我们可以看到**Albedo (RGB)**的纹理跟代码的关联如下代码所示: MainTex: _MainTex ("Albedo (RGB)", 2D) = "white" {} 在我们着色器代码中的CGPROGRAM代码块部分,纹理被定义为sampler2D类型,这是一种标准的2D纹理类型: sampler2D _MainTex; 紧接着下一行给我们展示了Input这个结构。这个结构就是surface 函数中得输入参数并且这个结构包含了一个叫做uv_MainTex的包组数组: struct Input { float2 uv_MainTex; }; 每一次调用**surf()函数的时候,对应3D模型中的包含_MainTex 这个UV的Input 结构都需要被渲染。标准着色器会知道uv_MainTex 跟_MainTex **是关联的,并且会自动初始化它。如果你真的很想了解到底UV是怎么从3D模型映射到2D纹理的话,你可以看看第三章, 理解光照模型。 终于,UV数据被用来在**surface **函数中展示成一张纹理: fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; 注意 U和V的取值范围都是从0到1,**(0,0)和(1,1)**相当于两个相对的角[可以想象成一个是左下角,一个是右上角]。如果你的纹理出现了颠倒的情况,试着把V的值也颠倒就能解决了。 额外内容 ...

December 11, 2020 · 1 min · 76 words · Link