一,单张纹理
好的,用户想学习Unity Shader中的单张纹理章节。我需要根据提供的搜索结果来整理相关内容。首先,查看搜索结果中的相关部分,特别是、、、、这几条,因为它们提到了基础纹理、单张纹理的实现方法和相关属性设置。
用户问题中的“单张纹理章节”可能涉及纹理映射的基本概念、UV坐标、纹理属性设置以及在Shader中的具体应用。需要分步骤讲解如何实现单张纹理的渲染,包括Properties块的定义、顶点着色器和片元着色器的处理,以及纹理采样和光照计算。
要注意引用来源的标注,每个要点后面要加上对应的角标,比如提到纹理类型和UV坐标,和讨论了纹理属性和Wrap Mode,中的代码示例展示了如何声明变量和使用TRANSFORM_TEX。需要确保每个步骤都有对应的引用,并且结合多个来源的信息,避免重复引用同一个网页。
同时,用户之前的问题涉及法线贴图,但现在转向单张纹理,可能希望了解基础部分,所以需要从最基础的纹理映射开始,逐步展开。结构上可以分为基本概念、实现步骤、关键代码示例、属性设置和常见问题,每个部分引用相关搜索结果。确保回答清晰、结构化,并且每个点都有对应的来源支持。
单张纹理章节核心知识点总结
- 纹理映射基础
定义:通过纹理映射(Texture Mapping)将2D图片逐纹素(Texel)映射到模型表面,控制模型颜色和细节。
UV坐标:顶点存储的纹理映射坐标(二维变量 (u,v)),通常归一化到 [0,1] 范围,用于采样纹理。
坐标系差异:
OpenGL纹理空间原点在左下角,DirectX在左上角,Unity默认采用OpenGL风格。 - 单张纹理实现步骤
2.1 声明纹理属性
在Shader的 Properties 块中定义纹理贴图及参数:
Properties {
_MainTex (“基础贴图”, 2D) = “white” {} // white
表示默认纯白色纹理:ml-citation{ref=“1,8” data=“citationList”}
_Color (“颜色调节”, Color) = (1,1,1,1) // 可选颜色混合参数:ml-citation{ref=“8” data=“citationList”}
}
需在CG代码中声明变量:
sampler2D _MainTex;
float4 _MainTex_ST; // 存储Tiling和Offset参数(xy为缩放,zw为偏移):ml-citation{ref=“4,8” data=“citationList”}
2.2 顶点着色器处理UV
通过 TRANSFORM_TEX 宏应用Tiling/Offset:
struct v2f {
float2 uv : TEXCOORD0;
// 其他数据(如顶点位置、法线等)
};
v2f vert(appdata_base v) {
v2f o;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // 应用缩放和偏移:ml-citation{ref=“1,8” data=“citationList”}
return o;
}
2.3 片元着色器采样与混合
采样纹理并与颜色参数混合,计算最终颜色:
fixed4 frag(v2f i) : SV_Target {
fixed4 texColor = tex2D(_MainTex, i.uv); // 采样纹理:ml-citation{ref=“1,6” data=“citationList”}
fixed3 albedo = texColor.rgb * _Color.rgb; // 混合颜色参数:ml-citation{ref=“8” data=“citationList”}
// 应用光照计算(如Lambert漫反射)
return fixed4(albedo, texColor.a);
}
- 纹理属性设置关键点
Texture Type:
单张纹理通常设为 Default,法线贴图需设为 Normal Map。
Wrap Mode:
Repeat:平铺纹理,适合无缝贴图;
Clamp:边缘拉伸,避免接缝处采样异常。
Filter Mode:
Point:像素风格,性能最优;
Bilinear/Trilinear:平滑过渡,质量更高但消耗更大。 - 常见问题与调试
UV坐标异常:检查模型是否导出正确UV,或调整 _MainTex_ST 参数。
颜色偏差:确保纹理导入设置中 sRGB 选项正确(颜色贴图启用,法线贴图禁用)。
性能优化:
移动端优先使用低分辨率纹理和 Bilinear 滤波;
复杂场景启用Mipmap减少远处纹理锯齿。 - 完整代码示例(Surface Shader)
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/SingleTextureExample" {
Properties {
_MainTex ("Base Texture", 2D) = "white" {}
_Color ("Color Tint", Color) = (1,1,1,1)
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed4 _LightColor0;
v2f vert (appdata v) {
v2f o;
// --- 方法1:Unity内置函数(注释掉以禁用)---
// o.pos = UnityObjectToClipPos(v.vertex);
// o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// o.worldNormal = UnityObjectToWorldNormal(v.normal);
// --- 方法2:手动计算(默认启用)---
o.pos = UnityObjectToClipPos(v.vertex); // 手动MVP转换:ml-citation{ref="3,4" data="citationList"}
o.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw; // 手动计算Tiling/Offset:ml-citation{ref="2,6" data="citationList"}
o.worldNormal = mul((float3x3)unity_ObjectToWorld, v.normal); // 手动法线转换:ml-citation{ref="4,5" data="citationList"}
return o;
}
fixed4 frag (v2f i) : SV_Target {
// 纹理采样与颜色混合
fixed4 texColor = tex2D(_MainTex, i.uv); // 采样主纹理:ml-citation{ref="1,2" data="citationList"}
fixed3 albedo = texColor.rgb * _Color.rgb;
// 光照计算(漫反射)
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float3 normal = normalize(i.worldNormal);
float diff = saturate(dot(normal, lightDir)*0.5+0.5); // 漫反射强度:ml-citation{ref="4,5" data="citationList"}
fixed3 finalColor = albedo * _LightColor0.rgb * diff;
return fixed4(finalColor, texColor.a);
}
ENDCG
}
}
FallBack "Diffuse"
}
二,法线纹理
Unity 法线贴图设置与工作原理
法线贴图对顶点的影响机制
法线贴图不会直接修改顶点坐标,而是通过扰动表面法线方向改变光照计算结果,从而在视觉上模拟凹凸效果。以下是具体实现原理与步骤:
一、顶点与法线的关系
顶点数据基础:
顶点包含位置(Position)、法线(Normal)和切线(Tangent)等属性,定义了模型的几何形状与表面朝向。
顶点着色器中,通过模型变换矩阵(如 UNITY_MATRIX_MVP)将顶点从模型空间变换到裁剪空间。
法线贴图的作用:
法线贴图存储的是切线空间下的法线偏移(RGB 通道映射到 [-1,1] 范围),而非顶点位置信息。
通过修改法线方向,改变光线反射方向,在固定顶点位置下模拟表面凹凸感。
将贴图设为 Normal Map 类型后,Unity 通过以下流程将其识别为物体法线方向,并参与光照计算:
**1. 贴图类型标记的作用
数据格式处理:标记为 Normal Map 后,Unity 会针对不同平台(如 PC、移动端)自动采用合适的压缩格式(如 DXT5nm)。
法线解压优化:通过 UnpackNormal 函数将法线贴图的颜色值(01)映射回三维法线向量(-11),例如:
glsl
Copy Code
float3 normalTangent = UnpackNormal(tex2D(_NormalMap, uv)); // 解压法线:ml-citation{ref=“1,3” data=“citationList”}
此步骤确保法线方向的正确性。
**2. 法线贴图的转换流程
切线空间→世界空间:
顶点着色器传递模型的切线(Tangent)、副切线(Binormal)和原始法线(Normal),构建 TBN 矩阵(Tangent-Binormal-Normal Matrix)。
float3x3 TBN = float3x3(
normalize(worldTangent),
normalize(worldBinormal),
normalize(worldNormal)
); // TBN矩阵构建:ml-citation{ref="4,5" data="citationList"}
片元着色器通过 TBN 矩阵将切线空间法线转换到世界空间:
float3 worldNormal = mul(normalTangent, TBN); // 切线→世界空间转换:ml-citation{ref="4,5" data="citationList"}
**3. 法线方向的应用
光照计算:转换后的世界空间法线参与光照模型(如 Blinn-Phong 或 PBR),直接影响漫反射和高光效果。例如:
float diff = saturate(dot(worldNormal, lightDir)); // 漫反射计算:ml-citation{ref="4,6" data="citationList"}
float3 halfDir = normalize(lightDir + viewDir);
float spec = pow(saturate(dot(worldNormal, halfDir)), _Gloss * 128); // 高光计算:ml-citation{ref="4,6" data="citationList"}
关键注意事项
*贴图导入设置: 必须将法线贴图类型设为 Normal Map,否则 UnpackNormal 无法正确解压数据。 Unity 默认法线贴图的 z 分量为正,仅存储 x、y 分量(z 通过计算得出)。
Shader 属性命名: 法线贴图变量需命名为 _BumpMap 或 _NormalMap,否则 Unity 内置管线可能无法识别。 非均匀缩放修正: 若模型存在缩放,需通过逆转置矩阵修正法线方向:*
**float3 worldNormal = mul(transpose(inverse((float3x3)unity_ObjectToWorld)), normal);
避免因缩放导致法线方向错误。
效果对比
未用法线贴图 使用法线贴图
表面平滑无细节 凹凸细节清晰,光影动态变化
依赖模型顶点法线 低模即可实现高精度效果**
通过上述流程,Unity Shader 能够将法线贴图的颜色数据解析为法线向量,并动态影响光照计算,最终实现高细节的表面效果。
法线纹理的两种计算方式对比
- 切线空间计算(Tangent Space)
核心逻辑:在切线空间下直接处理光照方向、视角方向,将光照方向转换到切线空间进行计算。
优势:
效率高:矩阵变换(如光照方向转换)可在顶点着色器完成,减少片元着色器计算量。
灵活性高:
支持 UV 动画(如水流、熔岩动态效果);
法线贴图可跨模型复用(记录相对法线信息,不受模型变形影响)。
可压缩:仅需存储法线贴图的 XY 分量(Z 方向由归一化推导得出)。
劣势:
全局效果精度低:
处理镜面反射、环境映射等全局效果时可能不够准确;
切线空间法线需逐顶点构造 TBN 矩阵,模型变形(如骨骼动画)可能导致误差。
2. 世界空间计算(World Space)
核心逻辑:将法线贴图从切线空间转换到世界空间,直接在世界坐标系下进行光照计算。
优势:
全局效果精准:
光照、反射等计算基于统一的世界坐标系,精度更高;
适用于复杂光照场景(如动态阴影、全局光照)。
实现直观:无需处理切线空间与模型空间的关系,逻辑更清晰。
劣势:
性能开销大:
TBN 矩阵构造和法线空间转换需在片元着色器执行,计算密集;
对低端设备不友好(如移动端)。
兼容性差:
法线贴图无法跨模型复用(存储绝对法线方向);
模型变形时需重新计算 TBN 矩阵。
适用场景总结
计算方式 推荐场景 不适用场景
切线空间计算 移动端/性能敏感场景、UV 动画、模型复用需求 高精度全局光照、动态模型变形
世界空间计算 PC/主机端、复杂光照场景(如镜面反射、环境映射) 低性能设备、需 UV 动画的场景
关键结论
效率优先选切线空间:适合性能敏感场景及动态效果需求;
效果优先选世界空间:需高精度全局光照时不可替代。
1,切线空间法线贴图实现原理与步骤
基本思路:再片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向,光照方向等进行计算。为此,我们需要再顶点着色器中把视角方向和光照方向从模型空间变到切线空间中
切线空间法线贴图通过顶点切线坐标系存储法线偏移,在 Shader 中通过 TBN 矩阵将法线转换到世界空间参与光照计算。以下是完整实现流程:
一、原理概述
切线空间特性:
切线空间由顶点切线(T)、副切线(B)、法线(N)构成,法线贴图存储的是该空间下的法线偏移。
在此空间下,法线方向无需依赖模型姿态,仅需将光照方向、视角方向等外部向量转换到切线空间,即可直接参与计算13。
核心步骤:
顶点着色器:将模型空间的光照方向、视角方向转换到切线空间。
片元着色器:采样法线贴图,与切线空间的光照方向、视角方向计算光照。
Shader "Custom/TangentSpaceNormal" {
Properties {
_MainTex ("Albedo", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT; // tangent.w用于副切线方向修正
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
// 切线空间下的光照方向与视角方向
float3 lightDirTS : TEXCOORD1; // Tangent Space Light Direction
float3 viewDirTS : TEXCOORD2; // Tangent Space View Direction
};
sampler2D _MainTex, _BumpMap;
v2f vert(appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
// 计算TBN矩阵
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
float3x3 TBN_World = float3x3(worldTangent, worldBinormal, worldNormal);
// 模型空间→切线空间转换矩阵
float3x3 TBN_Object = mul(TBN_World, (float3x3)unity_WorldToObject);
// 将光照方向、视角方向转换到切线空间
float3 lightDirObj = normalize(ObjSpaceLightDir(v.vertex)); // 模型空间光照方向
float3 viewDirObj = normalize(ObjSpaceViewDir(v.vertex)); // 模型空间视角方向
o.lightDirTS = mul(TBN_Object, lightDirObj); // 模型→切线空间
o.viewDirTS = mul(TBN_Object, viewDirObj);
return o;
}
fixed4 frag(v2f i) : SV_Target {
// 采样法线贴图(切线空间)
fixed3 normalTS = UnpackNormal(tex2D(_BumpMap, i.uv));
// 切线空间下的光照计算(以Blinn-Phong为例)
float3 lightDirTS = normalize(i.lightDirTS);
float3 viewDirTS = normalize(i.viewDirTS);
float3 halfDirTS = normalize(lightDirTS + viewDirTS);
// 漫反射
float diff = saturate(dot(normalTS, lightDirTS));
// 高光
float spec = pow(saturate(dot(normalTS, halfDirTS)), 32);
fixed4 albedo = tex2D(_MainTex, i.uv);
return albedo * _LightColor0 * (diff + spec);
}
ENDCG
}
}
}
2,世界空间法线贴图实现原理与步骤
顶点着色器阶段:
构建TBN矩阵的逆矩阵(Model→Tangent空间)。
将模型空间的法线、切线、副切线转换到世界空间,构建World→Tangent矩阵45。
片元着色器阶段:
采样法线贴图后,需通过矩阵将法线从切线空间变换到世界空间,再与光照方向计算。
Shader “Custom/WorldSpaceNormalMap” {
Properties {
_MainTex (“Albedo”, 2D) = “white” {}
_NormalMap (“Normal Map”, 2D) = “bump” {}
_NormalScale (“Normal Scale”, Range(0,2)) = 1.0
}
SubShader {
Tags { “RenderType”=“Opaque” }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldPos : TEXCOORD1;
// TBN矩阵相关向量
float3 worldNormal : TEXCOORD2;
float3 worldTangent : TEXCOORD3;
float3 worldBitangent : TEXCOORD4;
};
sampler2D _MainTex, _NormalMap;
float _NormalScale;
v2f vert (appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// --- 使用Unity内置函数计算 ---
// o.worldNormal = UnityObjectToWorldNormal(v.normal);
// o.worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
// --- 手动计算TBN矩阵 ---
o.worldNormal = mul((float3x3)unity_ObjectToWorld, v.normal);
o.worldTangent = mul((float3x3)unity_ObjectToWorld, v.tangent.xyz);
// 副切线计算(考虑手性与非均匀缩放)
float3 worldBitangent = cross(o.worldNormal, o.worldTangent) * v.tangent.w * unity_WorldTransformParams.w;
o.worldBitangent = worldBitangent;
return o;
}
float4 frag (v2f i) : SV_Target {
// 采样法线贴图并解包(切线空间→世界空间)
float3 tangentNormal = UnpackNormal(tex2D(_NormalMap, i.uv));
tangentNormal.xy *= _NormalScale;
tangentNormal.z = sqrt(1 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 构建TBN矩阵
float3x3 tbn = float3x3(
normalize(i.worldTangent),
normalize(i.worldBitangent),
normalize(i.worldNormal)
);
// 将法线从切线空间转换到世界空间
float3 worldNormal = mul(tbn, tangentNormal);
// 光照计算(世界空间)
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
float diffuse = max(0, dot(worldNormal, lightDir));
// 采样漫反射贴图
float4 albedo = tex2D(_MainTex, i.uv);
return albedo * diffuse;
}
ENDCG
}
}
}
关键逻辑解析
顶点着色器阶段
TBN矩阵构建:
使用内置函数时,直接调用 UnityObjectToWorldNormal 和 UnityObjectToWorldDir 转换法线、切线到世界空间。
手动计算时,通过 mul 与 unity_ObjectToWorld 矩阵完成变换,并计算副切线(需处理非均匀缩放与手性)。
片元着色器阶段
法线解包:UnpackNormal 用于解码法线贴图(支持压缩格式)。
TBN矩阵应用:通过矩阵乘法将切线空间法线转换到世界空间。
光照计算:所有方向向量统一在世界空间下计算漫反射。
渐变纹理(Ramp Texture)的核心概念与实现
渐变纹理是一种通过预定义颜色过渡规则控制表面光照效果的纹理技术,主要用于实现非真实感渲染(如卡通风格、插画风格)13。其核心原理与关键应用如下:
三,渐变纹理
一、基本原理
替代传统漫反射计算
传统Lambert模型通过法线与光源方向点积计算明暗,而渐变纹理将计算结果映射到0~1区间,作为UV坐标采样渐变纹理颜色13。
公式:
glsl
Copy Code
float halfLambert = dot(normal, lightDir) * 0.5 + 0.5; // 映射到[0,1]
float3 rampColor = tex2D(_RampTex, float2(halfLambert, 0.5));
颜色可控性
设计师可通过调整渐变纹理的颜色过渡曲线,精确控制物体表面的明暗层次与色调变化,例如实现硬边阴影或冷暖色调过渡。
二、核心优势
风格化渲染支持
渐变纹理通过离散的颜色分界强化轮廓线,使模型呈现卡通插画风格(见图1)。
性能高效
仅需一次纹理采样即可替代复杂的光照计算,适用于移动端或低性能设备。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 7/Ramp Texture" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_RampTex ("Ramp Tex", 2D) = "white" {}
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
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;
// Use the texture to sample the diffuse color
fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;
fixed3 diffuse = _LightColor0.rgb * diffuseColor;
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
4,遮罩纹理
一、技术原理
遮罩作用机制
遮罩纹理存储灰度值(单通道)或RGBA多通道数据,在渲染过程中通过纹理采样获取特定位置的遮罩值(如 r 通道),并与表面属性(如高光强度、漫反射系数)进行乘法运算,实现区域化控制。
公式示例(控制高光强度):
glsl
Copy Code
float mask = tex2D(_MaskTex, uv).r;
float3 specular = _LightColor0.rgb * pow(max(0, dot(N, H)), _Gloss) * mask;
(mask 为遮罩值,0表示完全屏蔽高光,1表示完全保留)。
数据映射规则
灰度值范围(0~1)对应属性影响的衰减程度,例如:
0(黑色):完全屏蔽效果(如无高光)
0.5(灰色):中等强度
1(白色):完全生效。
fixed4 frag(v2f i) : SV_Target {
// 采样基础颜色
fixed4 albedo = tex2D(_MainTex, i.uv);
// 计算光照方向与视角方向
float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 halfDir = normalize(lightDir + viewDir);
// 采样遮罩纹理(R通道控制高光强度)
float mask = tex2D(_MaskTex, i.uv).r;
// 计算高光反射
float spec = pow(max(0, dot(i.worldNormal, halfDir)), _Gloss);
float3 specular = _LightColor0.rgb * _Specular.rgb * spec * mask; // 遮罩值mask控制高光区域:ml-citation{ref="1,2" data="citationList"}
// 最终颜色混合
return fixed4(albedo.rgb + specular, 1.0);
}