C for Graphic:语言(动画)

        比如我喜欢看书,特别是“物理学”和“心理学”的书,我想通过shader实现简单的翻书效果,模拟一下真实现实中翻书的动效。首先,我创建一个符合要求的“书页”了,现实中书本都是以“书脊”为对称轴开合的,那么我们就要创建一个以虚拟“书脊”坐标系内的“书页”网格,如下:

        C for Graphic:语言(动画)

        因为前面我在图形辅助工具章节已经讲过创建rectangle拓扑网格了,所以这里我就不再重复讲解,代码如下:

        

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. [ExecuteInEditMode]
  5. public class YYPlane : MonoBehaviour {
  6. public bool Refresh = false;
  7. [SerializeField]public int xCount = 100; //单位方块x轴数量
  8. [SerializeField]public int yCount = 100; //单位方块y轴数量
  9. private float mCellLen = 0.1f; //单位小方块边长
  10. private Mesh mMesh;
  11. void Start()
  12. {
  13. CreateMesh();
  14. }
  15. void Update()
  16. {
  17. if (Refresh)
  18. {
  19. CreateMesh();
  20. Refresh = false;
  21. }
  22. }
  23. private void CreateMesh()
  24. {
  25. //构建一个任意单位长宽的长方形
  26. mMesh = new Mesh();
  27. int xPointCount = xCount + 1; //x轴网格点数量
  28. int yPointCount = yCount + 1; //y轴网格点数量
  29. int xyMeshPointCount = xPointCount * yPointCount; //网格顶点的数量
  30. int triangleCount = xCount * yCount * 2; //三角面数量(小正方形的两倍)
  31. //构建mesh网格的所有信息数组
  32. Vector3[] vertices = new Vector3[xyMeshPointCount];
  33. int[] triangles = new int[triangleCount * 3];
  34. Vector2[] uvs = new Vector2[xyMeshPointCount];
  35. //记录拓扑信息循环的间隔
  36. int triangleIndex = 0;
  37. for (int x = 0; x < xPointCount; x++)
  38. {
  39. for (int y = 0; y < yPointCount; y++)
  40. {
  41. int index = x + y * xPointCount;
  42. vertices[index] = new Vector3(x * mCellLen, 0, y * mCellLen);
  43. if (x < xCount && y < yCount)
  44. {
  45. //这里就是拓扑信息的循环计算,结合绘画的拓扑信息图算一下
  46. triangles[triangleIndex] = x + y * xPointCount;
  47. triangles[triangleIndex + 1] = x + (y + 1) * xPointCount;
  48. triangles[triangleIndex + 2] = x + (y + 1) * xPointCount + 1;
  49. triangles[triangleIndex + 3] = x + y * xPointCount;
  50. triangles[triangleIndex + 4] = x + (y + 1) * xPointCount + 1;
  51. triangles[triangleIndex + 5] = x + y * xPointCount + 1;
  52. triangleIndex += 6;
  53. }
  54. uvs[index] = new Vector2((float)x / (float)xCount, (float)y / (float)yCount);
  55. }
  56. }
  57. mMesh.vertices = vertices;
  58. mMesh.triangles = triangles;
  59. mMesh.uv = uvs;
  60. GetComponent<MeshFilter>().sharedMesh = mMesh;
  61. }
  62. }

          只在代码中进行了简单的注释,如果不理解的同学需要返回图形辅助工具那一专栏学习,效果图如下:

  C for Graphic:语言(动画)

        “书脊”坐标系参照unity坐标系。

        接着我们想象一下现实中翻书的效果,“书页”沿着“书脊”的z轴进行逆时针180度运动,这个应该很简单,使用z轴旋转矩阵或者圆内sin cos函数就可以处理,这里我要绘制一下方便理解,如下图:

        C for Graphic:语言(动画)

        这里我解释一下,书页OA经过逆时针θ角度的z轴旋转运动,到达书页OP,动点P坐标为P(cosθ*X,sinθ*X,Z),当然假如小伙伴们看过我之前的三维旋转矩阵博客的话,同时也就知道使用z轴旋转矩阵照样能达到同样的效果,那么我们使用三角函数或者z轴旋转矩阵进行定点变换处理不就完了?nonono,继续往下看。

        假如我们要模拟真实的“书页”效果,就一个死板板直挺挺的旋转根本不能满足效果。想象一下,真实书页翻动还会造成书页的“扭曲”的,页面可是一张薄薄的纸制品,翻动的时候会因为重力张力拉力等作用力产生弧线扭曲的效果,这个效果我们怎么处理呢?不知道大家记不记得初中时候学习方程函数,一个普通的一元二次函数的函数图像就接近于书本打开时拱起的弧度了,这里我绘制一下y = -a*(x-b)^2+c(a,b,c>0)的函数图像,如下图:

        C for Graphic:语言(动画)

       手绘弧线没那么精准,不过依旧看得出来,函数y = -a*(x-b)^2+c(a,b,c>0)通过调节a,b,c就可以达到让页面“弯曲”的效果,比如在进行三角函数sin cos进行“翻动的”同时,对页面y轴坐标进行y = -a*(x-b)^2+c(a,b,c>0)叠加处理,因为该方程函数具有对称性,所以我们可以通过调节参数达到“页面翻动”0-90度向“正面页”拱起,页面翻动90-180度向“背面页”拱起的两种对称结果,就达到了现实中的效果,为了验证一下我的猜想,实现代码如下:

        

  1. Shader "Unlit/VertexFragUnlitShader"
  2. {
  3. Properties
  4. {
  5. _FrontTex ("FrontTexture", 2D) = "white" {}
  6. _BackTex ("BackTexture",2D) = "white" {}
  7. _Radian("Radian",Range(0.4,2.74)) = 0.4 //这里使用弧度值,因为三角函数输入为弧度
  8. _Range("Range",Range(0.0,1.0)) = 0.01
  9. }
  10. SubShader
  11. {
  12. Tags { "RenderType"="Opaque" }
  13. LOD 100
  14. Cull off //因为是书页,所以正反面都需要显示
  15. Pass
  16. {
  17. CGPROGRAM
  18. #pragma vertex vert
  19. #pragma fragment frag
  20. #include "UnityCG.cginc"
  21. struct appdata
  22. {
  23. float4 vertex : POSITION;
  24. float2 uv : TEXCOORD0;
  25. };
  26. struct v2f
  27. {
  28. float2 frontuv : TEXCOORD0; /*记录正面uv*/
  29. float2 backuv : TEXCOORD1; /*记录背面uv*/
  30. float4 vertex : SV_POSITION;
  31. };
  32. sampler2D _FrontTex; /*书页正面纹理*/
  33. sampler2D _BackTex; /*书页背面纹理*/
  34. float4 _FrontTex_ST;
  35. float4 _BackTex_ST;
  36. float _Radian; /*翻动的弧度值*/
  37. float _Range; /*翻书弧度的增幅值*/
  38. v2f vert (appdata v)
  39. {
  40. v2f o;
  41. //创建一个字段用来处理顶点源坐标数据
  42. float4 zV;
  43. //平面的x值根据翻动的弧度进行cos函数周期处理
  44. zV.x = cos(_Radian)*v.vertex.x;
  45. //平面的y值根据翻动的弧度进行sin函数周期处理
  46. //同时,进行y = -a*x^2 + b*x + c的一元二次方程进行弧度拱起效果处理
  47. zV.y = sin(_Radian)*v.vertex.x + _Range*(-pow((v.vertex.x-1),2)+1);
  48. //平面的z值和w值就不需要改变了
  49. zV.z = v.vertex.z;
  50. zV.w = v.vertex.w;
  51. //这里再进行MATRIX_MVP变换处理,或者提前进行MVP处理一样的
  52. o.vertex = UnityObjectToClipPos(zV);
  53. //对页面正反贴图的uv进行计算
  54. o.frontuv = TRANSFORM_TEX(v.uv, _FrontTex);
  55. o.backuv = TRANSFORM_TEX(v.uv,_BackTex);
  56. //这里注意,因为翻书翻过来以后,uv的x是反过来的,所以要用1.0-u
  57. o.backuv.x = 1.0-o.backuv.x;
  58. return o;
  59. }
  60. fixed4 frag (v2f i) : SV_Target
  61. {
  62. fixed4 col = fixed4(0.0,0.0,0.0,1.0);
  63. //这里就是控制当翻动的弧度达到0.5π的时候也就是角度90度时,就显示页面背面贴图采样,小于0.5π就进行正面贴图采样
  64. if(_Radian<=1.57)
  65. {
  66. col = tex2D(_FrontTex, i.frontuv);
  67. }
  68. else{
  69. col = tex2D(_BackTex, i.backuv);
  70. }
  71. return col;
  72. }
  73. ENDCG
  74. }
  75. }
  76. }

        代码中我做了简单的注释,这个CG shader代码我只着重强调两点:

       ①.因为书页是正反双面都要显示,所以使用两张纹理贴图,同时创建两个纹理Texcoord字段进行储存,语义绑定到TEXCOORD0和TEXCOORD1(也就是储存图形流水线顶点阶段后的纹理坐标),当进行背面纹理uv计算时,记得x分量(或者u)是需要反向的也就是1.0-uv.x,才能在“翻到”背面时正常显示背面的纹理,然后在fragment片段函数中判断翻转的弧度进行正反面采样。

        ②.因为为了方便起见,我是用世界坐标系原点进行变换处理,所以“翻页”的时候处理使用cos(radian)*X处理x轴分量,sin(radian)*X + 调整后的方程函数处理y轴分量,z轴和w齐次扩展则不变,_Range为我额外加的一个效果增幅参数字段,为了调整好效果,当然“书脊”所在的仿射坐标系可以根据需要随意translate位移,只是会多一步translate位移矩阵或者直接操作vertex.xyz分量还原而已。

          最后我们看下效果,如下图:

    C for Graphic:语言(动画)

        当然翻书特效可以通过更复杂的函数图像去实现,到达非常符合现实的效果,这里我只是做了一个简单的CG shader来扩展学习到底着色器能干些什么稀奇古怪的事情,有兴趣的小伙伴可以自己动手实现日常生活中一些自己喜欢的特效。

       顺便贴两张“心理学”和“物理学”的图片,如下:

      C for Graphic:语言(动画)

        so,我们接下来继续。