后面有基于这篇文章重构过:Unity Shader - 简单山脉 - 顶点着色器重构法线
运行效果
噪点图
可以写了个C# 噪点图生产器,学习用,且方便生成噪点图
CSharp Code
主要是根据材质中的"NoiseTex"纹理来生成山脉网格的脚本,注释写的比较清楚
本来是想在shader 的vs阶段采样纹理做为顶点高度偏移处理,就可以实时的处理高度
但是,试过会有shader编译错误,所以我就放在csharp脚本来处理
我看openGL的资料说是可以在sm3.0以上就可以在VS采样纹理了,但结果还是不行:#pragma target 5.0都不行
下面脚本中的参数:rows, cols注意,分别都不要超过250,否则会有显示问题,原因不明,可能是因为单个mesh的顶点数太多,Unity底层限制单次传输的数量吗?
// jave.lin 2019.08.22
using UnityEngine;
public class CreateMountainPlane : MonoBehaviour
{
public Material mat;
// 不知道为哈:cols>300 && rows>300(反正超过一定数量),就会显示不正常,不会报错,不知道什么原因
public int cols = 250;
public int rows = 250;
public float uvScale = 1; // 纹理坐标缩放
public float xz_gaps = 5f;
public float height_scale = 50f;
public Texture2D noiseTex;
public bool brightnessSaturate; // 亮度饱和调整,将亮度的[n~m]调整到[0,1]==>([n,m]-n)*1f/m == [0,1]
private MeshFilter mf;
private MeshRenderer mr;
void Start()
{
noiseTex = noiseTex == null ? mat.GetTexture("_NoiseTex") as Texture2D : noiseTex;
if (brightnessSaturate)
{
var min = 1f;
var max = 0f;
// 先得到最低、最高亮度
for (int i = 0; i < noiseTex.width; i++)
{
for (int j = 0; j < noiseTex.height; j++)
{
var v = noiseTex.GetPixel(i, j).r;
if (v < min) min = v;
if (v > max) max = v;
}
}
if (max > 0)
{
// 将亮度的[n~m]调整到[0,1]==>([n,m]-n)*1f/(m-n) == [0,1]
var scale = 1f / (max - min);
for (int i = 0; i < noiseTex.width; i++)
{
for (int j = 0; j < noiseTex.height; j++)
{
var v = noiseTex.GetPixel(i, j).r;
v -= min;
v *= scale;
noiseTex.SetPixel(i, j, new Color(v, v, v));
}
}
noiseTex.Apply(true, false);
}
}
mf = gameObject.GetComponent<MeshFilter>();
if (mf == null) mf = gameObject.AddComponent<MeshFilter>();
mr = gameObject.GetComponent<MeshRenderer>();
if (mr == null) mr = gameObject.AddComponent<MeshRenderer>();
mr.sharedMaterial = mat;
mf.sharedMesh = CreateMesh(cols, rows);
}
private Mesh CreateMesh(int cs, int rs)
{
var result = new Mesh();
var quad_count = cs * rs; // 正方形数量
var triangle_count = quad_count * 2; // 三角形数量
var vertex_rows = rs + 1; // 行数
var vertex_cols = cs + 1; // 列数
var vertices_count = vertex_rows * vertex_cols; // 顶点数
var vertices = new Vector3[vertices_count]; // 顶点数组
var uvs = new Vector2[vertices_count]; // uv数组
var indices_count = triangle_count * 3; // 索引数量
var indices = new int[indices_count]; // 三角索引
for (int rIdx = 0; rIdx < vertex_rows; rIdx++)
{
for (int cIdx = 0; cIdx < vertex_cols; cIdx++)
{
var idx = rIdx * vertex_cols + cIdx;
var u = (float)cIdx / cs * uvScale;
var v = (float)rIdx / rs * uvScale;
uvs[idx] = new Vector2(u, v);
// 本想在vertex shader中来调整偏移的,但是,不知道unity中如何提高sm版本,因为使用#pragma target 5.0也不能在vs在采样纹理
// 所以我就在csharp脚本中来处理了
var height = noiseTex.GetPixelBilinear(u, v).r * height_scale;
vertices[idx] = new Vector3(cIdx * xz_gaps, height, rIdx * xz_gaps);
//Debug.Log($"x:{rIdx}, y:{cIdx}, u:{u}, v:{v}, height:{height}, vertices[{idx}]:{vertices[idx]}");
}
}
// 设置三角索引
var idx1 = 0;
for (int rIdx = 0; rIdx < rs; rIdx++)
{
for (int cIdx = 0; cIdx < cs; cIdx++)
{
/*
* 我们下面是逐个Quad来设置,不使用strip
* 所以每个Quad需要6个顶点(索引)
2______3 上一行
| /|
| / |
| / |
|/ |
0------1 本行
* */
var t0 = (rIdx + 1) * vertex_cols + cIdx; // idx:2, 上一行的第col index个,先偏移(rIdx + 1)*vertex_cols个(每行vertex_cols个,所以需要乘以vertex_cols)
var t1 = (rIdx + 1) * vertex_cols + cIdx + 1; // idx:3, 上一行的第col index + 1个
var t2 = rIdx * vertex_cols + cIdx; // idx:0, 本行第col index个
var t3 = (rIdx + 1) * vertex_cols + cIdx + 1; // idx:3, 上一行的第col index + 1个
var t4 = rIdx * vertex_cols + cIdx + 1; // idx:1, 本行第col index + 1个
var t5 = rIdx * vertex_cols + cIdx; // idx:0, 本行第col index个
// unity正面是clock(顺时方向),所以索引组合要注意顺序
indices[idx1++] = t0; // idx:2
indices[idx1++] = t1; // idx:3
indices[idx1++] = t2; // idx:0
indices[idx1++] = t3; // idx:3
indices[idx1++] = t4; // idx:1
indices[idx1++] = t5; // idx:0
}
}
result.vertices = vertices;
result.triangles = indices;
result.uv = uvs;
result.RecalculateNormals(); // 重新计算法线
result.RecalculateTangents(); // 重新计算切线
result.RecalculateBounds(); // 重新计算AABB
result.UploadMeshData(true); // 上传到GPU RAM,并销毁CPU RAM数据
return result;
}
}
生成网格效果
200x200的网格
4W+个顶点,8W个三角面
Shader Code
山脉的shader
// jave.lin 2019.08.22
Shader "Test/NoiseMountain" {
Properties {
_NoiseTex ("NoiseTex", 2D) = "white" {} // 主要影响草地、岩石、峰谷雪地的噪点数据
[NoScaleOffset] _NoiseTex_Blend ("NoiseTex_Blend", 2D) = "white" {} // 用于混合影响_NoiseTex的另一张噪点数据,可以不设置,就不会有影响
_NoiseTex_Blend_Intensity ("NoiseTex_Blend_Intensity", Range(0,1)) = 1 // 用于设置_NoiseTex_Blend_Intensity对_NoiseTex影响的强度,0:完全不影响,如同没设置一样,1:完全影响,即:完全取代_NoiseTex;其实就插值过渡
_Noise1_GrassHasRock_Tex ("Noise1_GrassHasRock_Tex", 2D) = "black" {} // 用于混合_NoiseTex中,属于Grass草地那段数据范围,可然山谷出出现岩石纹理的混合
_Noise1_GrassHasRock_Tex_Intensity ("Noise1_GrassHasRock_Tex_Intensity", Range(0,10)) = 1 // _Noise1_GrassHasRock_Tex 草地中混合岩石的强度
_GrassTex ("GrassTex", 2D) = "white" {} // 草地纹理
_RockTex ("RockTex", 2D) = "white" {} // 岩石纹理
_PeakColor ("_PeakColor", Color) = (0.95,0.95,1,1) // 山峰色调
_Valley ("Valley", Range(0,1)) = 0.3 // 山谷划分界限
_Peak ("Peak", Range(0,1)) = 0.85 // 山峰划分界限
_GradientThresold ("GradientThresold", Range(0,1)) = 0.1 // 山谷、山腰、山峰的过渡幅度阈值
_Gradient ("Gradient", Range(0,1)) = 1 // 过渡强度,0:完全硬生生的,没有渐变过渡,1:完全渐变过渡,一般0.5就好
_SpecularGlossy ("SpecularGlossy", Range(0, 1)) = 0.16 // 整体山脉高光反射平滑度,山谷、山腰、山峰,分别在fs中有硬编码的反射强度系数控制,当然也可以在properties中公开设置
_SpecularIntensity ("SpecularIntensity", Range(0, 1)) = 0.5 // 整体山脉高度反射强度
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 100
Pass {
CGPROGRAM
#pragma target 5.0
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f {
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
float3 wPos : TEXCOORD2;
float4 valley_peak_uv : TEXCOORD3; // 山谷、山峰的纹理UV
float4 grass_has_rock_uv : TEXCOORD4; // 草地混合岩石的纹理UV
};
sampler2D _NoiseTex;
sampler2D _NoiseTex_Blend;
fixed _NoiseTex_Blend_Intensity;
float4 _NoiseTex_ST;
sampler2D _Noise1_GrassHasRock_Tex;
float4 _Noise1_GrassHasRock_Tex_ST;
fixed _Noise1_GrassHasRock_Tex_Intensity;
sampler2D _GrassTex;
float4 _GrassTex_ST;
sampler2D _RockTex;
float4 _RockTex_ST;
fixed4 _PeakColor;
float _Valley;
float _Peak;
float _Gradient;
float _GradientThresold;
fixed _SpecularGlossy;
fixed _SpecularIntensity;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _NoiseTex);
o.valley_peak_uv = fixed4(
TRANSFORM_TEX(v.uv, _GrassTex),
TRANSFORM_TEX(v.uv, _RockTex));
o.grass_has_rock_uv = fixed4(
TRANSFORM_TEX(v.uv, _Noise1_GrassHasRock_Tex), 0, 0);
o.normal = UnityObjectToWorldNormal(v.normal);
o.wPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
// sample the texture
const fixed grassSpecScale = 0.3; // 草地高光强度系数
const fixed rockSpecScale = 0.75; // 岩石高光强度系数
const fixed peakSpecScale = 1; // 山峰高光强度系数
fixed4 col = tex2D(_NoiseTex, i.uv) * // 混合_NoiseTex与_NoiseTex_Blend,根据:_NoiseTex_Blend_Itensity
lerp(fixed4(1,1,1,1), tex2D(_NoiseTex_Blend, i.uv), _NoiseTex_Blend_Intensity);
fixed specularScale = peakSpecScale;
float d = 0;
float t = 0;
float n = col.r;
fixed4 rockCol = tex2D(_RockTex, i.valley_peak_uv.zw);
fixed4 pColor = lerp(rockCol, _PeakColor, _PeakColor.a);
if (n <= _Valley) { // 小于山谷高度的处理
d = _Valley - n; // 与山谷界限处的距离
t = saturate(d / _GradientThresold * _Gradient); // 根据距离与阈值作为插值的百分比系数
col = lerp(rockCol, tex2D(_GrassTex, i.valley_peak_uv.xy), t * _Noise1_GrassHasRock_Tex_Intensity); // 岩石与草地的插值
specularScale = lerp(rockSpecScale,grassSpecScale,t); // 岩石与草地高光系数过渡
fixed4 noise1 = tex2D(_Noise1_GrassHasRock_Tex, i.grass_has_rock_uv.xy); // 岩石与草地的纹理混合噪点过渡
col = lerp(col, rockCol, pow(noise1.r, _Noise1_GrassHasRock_Tex_Intensity));
} else if (n > _Valley && n < _Peak) { // 大于山谷、小于山峰高度处理
col = rockCol;
specularScale = rockSpecScale;
} else { // 山峰处理
d = n - _Peak;
t = d / _GradientThresold;
specularScale = lerp(rockSpecScale,peakSpecScale,t); // 岩石与山峰高光系数插值过渡
col = lerp(rockCol, pColor, t); // 岩石与山峰颜色插值过渡
}
// diffuse
half3 L = normalize(_WorldSpaceLightPos0.xyz);
half3 N = normalize(i.normal);
half LdotN = dot(L, N);// * 0.5 + 0.5;
fixed3 diffuse = col.rgb * LdotN;
// specular
half3 specular = 0;
half3 V = normalize(_WorldSpaceCameraPos.xyz - i.wPos);
half3 H = normalize(L + V);
if (LdotN > 0) {
half HdotN = max(0, dot(H, N)); // blinn-phone
specular = _LightColor0.rgb * pow(HdotN, _SpecularGlossy * 100) * _SpecularIntensity;
specular *= specularScale;
}
// ambient
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
return fixed4(diffuse + specular + ambient, 1);
}
ENDCG
}
}
}