Shader入门精要项目链接:
相关章节
一、立方体纹理
立方体纹理是环境映射的一种实现方法,环境映射可以模拟物体周围的环境,而使用环境映射的物体会看起来像金属一样反射出周围环境。
1.1 立方体纹理制作流程(Unity2017)
需要拖拉六张图片分别作为立方体纹理的六个面。
另一种做法:百度找到下方图片(我会提供),导入unity
创建出来后,我们创建一个材质球Material,然后进行如下操作,最终把立方体纹理拖放到材质球的Cubemap(HDR)上。
二、环境反射、环境映射、菲涅尔反射用例
2.1 环境反射(金属效果)
Shader "MilkShader/Ten/M_Reflection"
{
Properties
{
//立方体纹理
_Cubemap ("CubeMap", Cube) = "_Skybox" {}
//模型材质颜色
_Color ("Color Tint", Color) = (1,1,1,1)
//自定义反射颜色
_ReflectColor ("Reflection Color", Color) = (1,1,1,1)
//反射比:为1时则百分百反射出周围环境,否则是原本模型颜色
_ReflectAmount ("Reflect Amount", Range(0, 1)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float3 worldViewDir : TEXCOORD2;
float3 worldRef1 : TEXCOORD3;//反射向量
SHADOW_COORDS(4)
};
samplerCUBE _Cubemap;
fixed4 _Color;
fixed4 _ReflectColor;
fixed _ReflectAmount;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
//根据view和normal经过reflect函数计算出反射向量
o.worldRef1 = reflect(-o.worldViewDir, o.worldNormal);
TRANSFER_SHADOW(o)
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//最终反射颜色:texCUBE函数:从_Cubemap立方体纹理通过反射向量进行采样,再与自定义反射颜色相乘
fixed3 reflection = texCUBE(_Cubemap, i.worldRef1).rgb * _ReflectColor.rgb;
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal, worldLightDir));
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)
//最终输出颜色:环境光 + 漫反射颜色->反射颜色的插值_ReflectAmount 再乘以 (atten)
return fixed4(ambient + lerp(diffuse, reflection, _ReflectAmount) * atten , 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit"//包含阴影处理的一个内置shader
}
2.2 环境折射
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "MilkShader/Ten/M_Refraction"
{
Properties
{
//立方体纹理
_Cubemap ("Cubemap", Cube) = "_Skybox" {}
//自定义折射颜色
_RefractColor ("Refract Color", Color) = (1,1,1,1)
//折射比:为1时则完全折射周围环境
_RefractAmount ("Refract Amount", Range(0,1)) = 1
//入射光所在空间的介质/折射光所在空间的介质,例如:空气/玻璃 = 1/1.5
_RefractRatio("Refraction Ratio", Range(0.1, 1)) = 0.5
//自定义材质颜色
_Color ("Color Tint", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float3 worldViewDir : TEXCOORD2;
float3 worldRefractDir : TEXCOORD3;//折射向量
SHADOW_COORDS(4)
};
samplerCUBE _Cubemap;
fixed3 _Color;
fixed3 _RefractColor;
fixed _RefractAmount;
fixed _RefractRatio;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
//使用refract函数(取反后的归一化视口向量, 归一化法线向量, 折射介质比) 返回折射向量
o.worldRefractDir = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);
TRANSFER_SHADOW(o)
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal, worldLightDir));
//与反射同理,这里用折射向量进行采样
fixed3 refraction = texCUBE(_Cubemap, i.worldRefractDir).rgb * _RefractColor.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos)
//与反射同理的处理
return fixed4(ambient + lerp(diffuse, refraction, _RefractAmount) * atten, 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit"
}
2.3 菲涅尔反射
菲涅尔反射在生活中的例子:
你站在一个湖面,离你越近的湖面越清晰,越远的湖面会有反射光,如果湖面很清澈,会反射出天空。如下图:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "MilkShader/Ten/M_Fresnel_Schlick"
{
Properties
{
//材质颜色
_Color ("Color Tint", Color) = (1,1,1,1)
//自定义环境反射颜色
_ReflectColor ("Reflection Color", Color) = (1,1,1,1)
//立方体纹理
_Cubemap ("Cubemap", Cube) = "_Skybox" {}
//菲涅尔反射影响系数 为1时纯环境反射,为0时纯菲涅尔反射
_FresnelScale ("Fresnel Scale", Range(0,1)) = 0.5
//菲涅尔反射的指数部分(通常是5)
_FresnelPow("Fresnel Pow", Float) = 5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal :NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal :TEXCOORD0;
float3 worldPos : TEXCOORD1;
float3 worldViewDir : TEXCOORD2;
float3 worldReflectDir : TEXCOORD3;
SHADOW_COORDS(4)
};
samplerCUBE _Cubemap;
fixed4 _Color;
fixed4 _ReflectColor;
fixed _FresnelScale;
float _FresnelPow;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
o.worldReflectDir = reflect(-o.worldViewDir, o.worldNormal);
TRANSFER_SHADOW(o)
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldViewDir);
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 reflection = texCUBE(_Cubemap, i.worldReflectDir).rgb * _ReflectColor.rgb;
UNITY_LIGHT_ATTENUATION(atten, i,i.worldPos);
//菲涅尔反射公式 f + (1-f)*pow((1-dot(viewDir, normal),5) 结果值为用于漫反射与反射颜色进行差值运算的百分比(0,1)
fixed fresnel = _FresnelScale + (1-_FresnelScale) * pow(1-dot(worldViewDir, worldNormal), _FresnelPow);
return fixed4(ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten, 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit"
}
原理:在1.1 环境反射上进行扩展,将漫反射和反射颜色的差值运算的插值百分比,换为菲涅尔反射计算出的结果值,如下代码
//菲涅尔反射公式 f + (1-f)*pow((1-dot(viewDir, normal),5) 结果值为用于漫反射与反射颜色进行差值运算的百分比(0,1)
fixed fresnel = _FresnelScale + (1-_FresnelScale) * pow(1-dot(worldViewDir, worldNormal), _FresnelPow);
return fixed4(ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten, 1.0);
Schlick菲涅尔反射公式: f + (1-f) * pow((1-v·n), 5) 【上面例子用的是这个,你可以试试Empricial菲涅尔反射公式】
Empricial菲涅尔反射公式:max(0, min(1, bias + scale * pow((1 - v·n), power)))
下面公式,bias scale power都是自定义float参数,至于范围值是多少,可以自己调到满意为止,反正只要知道这个公式计算的结果值是用于作为漫反射和环境反射颜色的插值百分比的就行。(什么?你不懂什么叫插值!?百度吧亲)
[个人见解] 解释为什么菲涅尔反射公式(Schlick)的式子中会有视角向量和法线向量的点积出现,而不是其他向量。(加深理解)
我们知道最上面所说的生活中的“湖面”就是菲涅尔反射的效果,离摄像机越近的湖面越清晰(注意:这是理想情况下),从摄像机中心点到湖面上的点称谓“视角向量”,摄像机越近的湖面,视角向量与法线向量(湖面垂直线)的角度会越小,从而dot(v,n)越大(越接近1);摄像机越远的湖面,视角向量与法线向量(湖面垂直线)的角度会越大,从而dot(v,n)越小(越接近0),如下图
(横轴代表离摄像机距离, 纵轴代表(v·n)结果值)
f + (1-f) * pow((1-v·n), 5) 先忽略掉 f 和 pow的影响,式子只剩下 1 - (v·n) ,为什么会用 1 减去(v·n),而不是直接用(v·n)呢?
因为(v·n)在实际运算中如上图,离摄像机越远的,(v·n)越接近0,而这个结果值是用于插值的,即:
lerp(漫反射颜色, 天空颜色, 结果值), 当越远,则结果值为0时, 那直接插值得到 漫反射颜色,这明显不对,因为越远的湖面,应该反射出天空颜色,此时就要用 1-(v·n) 如下图
(横轴代表离摄像机距离, 纵轴代表1-(v·n)结果值)
此时,当越近时,结果值趋向0,输出颜色趋向漫反射颜色;当越远时,结果值趋向1,输出颜色趋向环境反射颜色(天空颜色)
再分析菲涅尔反射公式中的 f 值, 当f值为1时,公式变为: 1 + 0 * pow((1-v·n), 5) 即 1 ,那么输出颜色为环境反射颜色。
当f值为0时,公式变为:pow((1-v·n), 5) 此时是纯菲涅尔反射(符合“湖面”效果),但是实际上我测试出效果并不是很好,所以我将pow(...,5) 这个求幂的5,改小了,上面那个图的变化幅度就会越大,从而得出更大的效果。所以我在Shader上使用的是
//菲涅尔反射公式 f + (1-f)*pow((1-dot(viewDir, normal),5) 结果值为用于漫反射与反射颜色进行差值运算的百分比(0,1) fixed fresnel = _FresnelScale + (1-_FresnelScale) * pow(1-dot(worldViewDir, worldNormal), _FresnelPow);
_FresnelPow是公式的幂部分变量,如果你想更加精确地调整,可以思考用一个遮罩纹理的某个通道进行片元级别控制求幂系数,以此达到更好的效果。
(纯环境反射)
(环境反射+菲涅尔反射控制插值比)
当Fresnel Scale为1时,纯环境反射;
当Fresnel Scale为0时,可见边缘部分会反射出环境,中间部分离摄像机越近则是漫反射颜色。
三、渲染纹理
将摄像机的渲染图像输出到一张纹理上,而这张纹理则叫“渲染纹理”。
3.1 渲染纹理制作与用法
将创建出来的渲染纹理放置于Camera组件的Target Texture
选中摄像机物体后Scene窗口右下角小窗口是选中摄像机的渲染图像,它会被存储到Target Texture指定的渲染纹理中
3.2 镜子效果
使用创建一个摄像机用于渲染镜子位置看向物体的图像
创建一个Quad面板,将其大小调整为上方摄像机的近截面大小,位置调整到摄像机位置,角度调整为摄像机朝向。
(所选物体是Quad面板,Quad面板必须正面朝向摄像机的朝向,而不是反面!)
Shader如下:
Shader "MilkShader/Ten/M_Mirror"
{
Properties
{
_MainTex ("Main Tex", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue" ="Geometry"}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
fixed4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//水平方向进行翻转
o.uv = v.uv;
o.uv.x = 1 - o.uv.x;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
FallBack Off
}
材质球
Main Tex是上方创建的渲染纹理,Shader做了一个水平翻转,因为镜子的情况就是水平翻转后的显示结果。
3.3 玻璃效果
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "MilkShader/Ten/M_GlassRefraction"
{
Properties
{
//主纹理
_MainTex ("Texture", 2D) = "white" {}
//法线纹理
_NormalMap ("NormalMap", 2D) = "white" {}
//立体环境纹理
_Cubemap ("Cubemap", Cube) ="_Skybox"{}
_RefractAmount ("Refraction Amount", Range(0,1)) = 1.0 //折射系数
_OffsetW ("Offset Weight/Distortion", Range(0,100)) = 10//偏移权值
}
SubShader
{
//Queue渲染队列为透明队列,是为了不透明物体全部渲染出来之后再渲染玻璃
Tags { "RenderType"="Opaque" "Queue" = "Transparent"}
LOD 100
GrabPass { "_GrabTex" } //指定屏幕截图贴图名称
Pass
{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 tangent : TEXCOORD1;
float3 normal :TEXCOORD2;
};
struct v2f
{
float4 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
//TtoW0,1,2 存储了切线转世界的变换矩阵每一行, w分量存储世界位置相应的分量
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
float4 scrPos : TEXCOORD4;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalMap;
float4 _NormalMap_ST;
samplerCUBE _Cubemap;
fixed _RefractAmount;
sampler2D _GrabTex; //屏幕图像纹理(由GrabPass{"_GrabTex"}确定纹理变量名为_GrabTex)
float4 _GrabTex_TexelSize; //屏幕图像像素大小,变量名为"纹理变量名_TexelSize"
float _OffsetW;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv.xy, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.uv.xy, _NormalMap);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = mul(unity_ObjectToWorld, v.tangent);
float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
//如果这里不懂是什么,你可以直接理解为取切线向量xyz作为矩阵第一列,副法线向量xyz为第二列,第三列是法线项链,第四列是世界坐标位置
//前3*3是一个切线空间转世界空间的矩阵,第四列是存储世界位置的
//在《Shader入门精要》第七章——基础纹理的法线纹理有教学讲解!
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
//计算屏幕空间下的顶点位置 ComputeGrabScreenPos(裁剪空间的顶点位置) 它是一个内置函数
o.scrPos = ComputeGrabScreenPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
//获取切线空间下的法线纹理的法线向量, UnpackNormal操作不懂的可以看《Shader入门精要》第七章——基础纹理的法线纹理部分
fixed3 tangentSpaceNormal = UnpackNormal(tex2D(_NormalMap, i.uv.zw));
//将法线向量xy值*偏移权重*像素大小得到偏移量
float2 offset = tangentSpaceNormal.xy * _OffsetW * _GrabTex_TexelSize.xy;
//偏移屏幕空间下的顶点位置
i.scrPos.xy = offset + i.scrPos.xy;
//通过归一化的屏幕空间下的顶点位置进行从_GrabTex(屏幕纹理)采样获取模拟折射颜色
fixed3 refractCol = tex2D(_GrabTex, i.scrPos.xy/i.scrPos.w).rgb;
//将法线向量从切线空间转换到世界空间《Shader入门精要》第七章——基础纹理之法线纹理讲解!
fixed3 worldSpaceNormal = normalize(half3(dot(i.TtoW0.xyz, tangentSpaceNormal),dot(i.TtoW1.xyz, tangentSpaceNormal),dot(i.TtoW2.xyz, tangentSpaceNormal)));
//环境反射的反射向量
fixed3 reflectDir = reflect(-worldViewDir, worldSpaceNormal);
//获取主纹理颜色
fixed4 texCol = tex2D(_MainTex, i.uv.xy);
//最终反射颜色 = 环境反射颜色 * 主纹理颜色
fixed3 reflectCol = texCUBE(_Cubemap, reflectDir).rgb * texCol.rgb;
//最终输出颜色RGB = 反射颜色 和 模拟折射颜色 混合,其中_RefractAmount=1时候,纯折射,否则纯反射
fixed3 finalCol = reflectCol * (1- _RefractAmount) + refractCol * _RefractAmount;
return fixed4(finalCol, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
四、程序纹理
一个动态生成程序纹理的脚本(Shader入门精要)提供:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class M_ProceduralTextureGeneration : MonoBehaviour {
public Material material = null;
[SerializeField, SetProperty("textureWidth")]
private int m_textureWidth = 512;
public int textureWidth
{
get { return m_textureWidth; }
set
{
m_textureWidth = value;
_UpdateMaterial();
}
}
[SerializeField, SetProperty("backgroundColor")]
private Color m_backgroundColor = Color.white;
public Color backgroundColor
{
get
{
return m_backgroundColor;
}
set
{
m_backgroundColor = value;
_UpdateMaterial();
}
}
[SerializeField, SetProperty("circleColor")]
private Color m_circleColor = Color.yellow;
public Color circleColor
{
get { return m_circleColor; }
set
{
m_circleColor = value;
_UpdateMaterial();
}
}
[SerializeField, SetProperty("blurFactor")]
private float m_blurFactor = 2.0f;
public float blurFactor
{
get { return m_blurFactor; }
set { m_blurFactor = value;
_UpdateMaterial();
}
}
private Texture2D m_generateTexture = null;
private void Start()
{
if (material == null)
{
Renderer renderer = gameObject.GetComponent<Renderer>();
if(renderer == null)
{
Debug.LogWarning("can not find a renderer.");
return;
}
material = renderer.sharedMaterial;
}
_UpdateMaterial();
}
private void _UpdateMaterial()
{
if(material != null)
{
m_generateTexture = _GenerateProceduralTexture();
material.SetTexture("_MainTex", m_generateTexture);
}
}
private Texture2D _GenerateProceduralTexture()
{
Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth);
float circleInterval = textureWidth / 4.0f;
float radius = textureWidth / 10.0f;
float edgeBlur = 1.0f / blurFactor;
for (int w = 0; w < textureWidth; w++)
{
for (int h = 0; h < textureWidth; h++)
{
Color pixel = backgroundColor;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
Vector2 circleCenter = new Vector2(circleInterval * (i + 1), circleInterval * (j+1));
float dist = Vector2.Distance(new Vector2(w, h), circleCenter) - radius;
Color color = _MixColor(circleColor, new Color(pixel.r, pixel.g, pixel.b, 0.0f), Mathf.SmoothStep(0f, 1.0f, dist * edgeBlur));
pixel = _MixColor(pixel, color, color.a);
}
}
proceduralTexture.SetPixel(w, h, pixel);
}
}
proceduralTexture.Apply();
return proceduralTexture;
}
private Color _MixColor(Color color0, Color color1, float mixFactor)
{
Color mixColor = Color.white;
mixColor.r = Mathf.Lerp(color0.r, color1.r, mixFactor);
mixColor.g = Mathf.Lerp(color0.g, color1.g, mixFactor);
mixColor.b = Mathf.Lerp(color0.b, color1.b, mixFactor);
mixColor.a = Mathf.Lerp(color0.a, color1.a, mixFactor);
return mixColor;
}
}
材质球的Shader必须要有一个变量名为"_MainTex",其他的都不重要,上图SingleTexture是一个高光反射的Shader
代码比较简单,即生成一个中间带有3*3的圆点纹理,背景颜色、圈颜色、间距、圈外围渐变程度可控,总体而言,程序纹理是一种更复杂、更多表现的纹理,上面代码只是一种简单示例演示。