文章目录
一、前言
嗨 ,大家好,我是新发。
做3D
项目的同学应该对法线贴图不陌生,新手同学可能一开始比较懵逼 ,什么是表面法线?什么是法线贴图?为什么法线贴图长这个样子?法线贴图有什么用?等等这些问题,今天,我就带大家来一次【法线探险之旅】吧~
二 、直观感受法线贴图
首先我们先来直观感受下法线贴图的效果 ,我们打开下面这个网站,
地址:https://cpetry.github.io/NormalMap-Online/
这个网站可以在线生成法线贴图,中间那张偏蓝紫色的图片就是法线图了 ,右边是应用了这张法线贴图的平面,可以看到这个平面具有了凹凸的效果。
三、表面法线
1、表面法线的概念
首先我们需要先知道什么是表面法线 。Unity
官方的解释是:
The surface angle can be represented as a line protruding in a perpendicular direction from the surface, and this direction (which is a vector) relative to the surface is called a “surface normal”, or simply, a normal.
翻译成人话就是垂直于模型表面的向量就是表面法线,为了方便大家理解 ,我刻意做了个Demo
来演示表面法线 ,如下:
注:显示表面法线的代码见文章末尾附录:
NormalVisualizerWithGizmo.cs
代码。
我们的3D
模型是由网格组成的,一般都是由一个个三角网格组成,一个三角网格就是一个小平面 ,垂直于这个平面的向量就是法线了。
2 、空间与坐标系
在Unity
中,我们常听到的是世界坐标和局部坐标,对应的空间是世界空间和局部空间;而我们的法线是基于切线空间的 ,在介绍切线空间之前,我们先温习一下世界空间和局部空间 。
2.1 、世界空间——世界坐标系
世界坐标的方向是绝对的,如果我们从电脑屏幕往里看 ,则世界坐标系的x
轴(红色)朝右,y
轴(黄绿色)朝上,z
轴(蓝色)朝屏幕里面 ,像这样子:
注:了演示我特意去找了个笔记本的模型,把笔记本的贴图扣成透明的,方便透过屏幕看到屏幕里面的妹子 ,效果还不错吧~
2.2、局部空间——局部坐标系
局部坐标系是物体内部的一个相对的坐标系 ,局部坐标系为随着物体的位移、旋转而发生变化,我们可以在Unity
的工具栏这里切换世界坐标系和局部坐标系,如下:
如果你把一个物体A
作为另一个物体B
的子物体 ,则我们在Inspector
界面看到物体A
的坐标为局部坐标,只有当一个物体没有父物体的时候,Inspector
显示的坐标才为世界坐标。
2.3 、切线空间——切线坐标系
或许你也常听说过UV
坐标 ,其实,纹理的UV
坐标就是切线空间的x
轴和y
轴,不过U、V
坐标的范围是0~1
。
注:为什么用
UV
这两个字母呢?因为XYZ
三个字母被用了 ,所以用UV
。
聪明的你肯定发现了,如果再加上一个W
轴,不就构成了UVW
坐标系了吗?没错 ,这个就是切线空间坐标系了,不过切线坐标系的轴的命名有另一套叫法:TBN
,为了方便对照 ,我画个表格:
切线空间坐标轴 | 名称 | 对应关系 |
---|---|---|
T | 切线轴(tangant) | 对应x轴 ,或u轴 |
B | 副切线轴(bitangent) | 对应y轴,或v轴 |
N | 法线轴(normal) | 对应z轴,或w轴 |
模型网格的每个三角网格都有各自的切线空间 ,而我们的法线向量就是基于切线空间坐标系的。
2.4、小结
法线向量就是基于切线空间坐标系的,切线坐标系的三个轴是T 、B、N
。
四、法线贴图
有了表面法线数据,那我们如何存储这些法线数据呢?
法线是一个三维向量 ,比如(0, 0, 1)
,正好,我们的RGB
颜色也是一个三维数据 ,所以,我们就可以把法线信息使用RGB
颜色来存储成为一张图片了 。
1 、法线贴图如何存储法线数据
在切线空间坐标系中,正常情况下 ,法线垂直于表面,也就是法线垂直于TB
平面(或者UV
平面),这种情况 ,法线向量为(0, 0, 1)
,如果对应成归一化的RGB
就是蓝色了,
可是我们是可以让法线朝者TB
轴的负方向偏的,比如(-0.1, -0.4, 1)
,但,归一化的RGB
颜色是范围是0 ~ 1
,它不能表示负数 ,所以,并不是简单地把法线向量作为归一化的RGB
颜色值进行存储,而是经过了如下的变换:
rgb = (normal + 1) / 2
假设法线向量为(0, 0, 1)
,那么通过公式计算出来的归一化RGB
则为(0.5, 0.5, 1)
,对应成颜色值就是(128, 128, 255)
,效果如下:
2、有法线贴图与无法线贴图效果对比
我们先看一下上面的笔记本电脑模型的法线贴图 ,看到这里,你应该知道为什么法线贴图的主色调是偏蓝紫色了吧~
模型本身是平平的,如下:
没有法线贴图的效果是这样的:
加上法线贴图后的效果是这样的:
法线贴图的作用就是修改光照的反射使其看起来有凹凸感 ,这样我们的模型可以减少很多面数 ,凹凸细节通过法线贴图来实现,是不是很聪明的做法呢~
3、徒手画法线贴图凹凸效果
现在,我们知道了法线贴图的原理 ,所以,理论上我们是可以徒手画出一个有凹凸效果的法线贴图的。
说干就干!
先用ProBuilder
建个模,此处建模纯粹是要表达我想要的凹凸效果 ,
注:如果大家对
ProBuilder
建模感兴趣的话,我可以下期出个教程~
接着,把模型抹平 ,我们要用法线贴图来实现这个凸起的视觉效果,
打开PhotoShop
,开始画法线贴图 ,先给底图铺上颜色(128, 128, 255)
,
先画左边的斜面,假设斜面与平面的角度是45度
,
它的法线向量是(-0.5, 0, 1)
,
根据公式rgb = (normal + 1) / 2
计算出RGB
颜色为(64, 128, 255)
接着画右边的斜面,它的法线是(0.5, 0, 1)
,计算出RGB
颜色为(192, 128, 255)
,
一次类推,最终各个块的颜色值如下:
保存为JPG
格式,如下 ,
把法线贴图导入到Unity
中,应用到材质球的Normal Map
上,
效果如下:
五 、场景法线颜色可视化
既然我们可以知道法线信息 ,也可以知道法线对应的RGB
颜色,那么,我们可否把场景中的所有物体的法线都转成颜色并显示为出来呢?
理论存在 ,实践开始。
1、shader,法线信息转rgb颜色
我们写一个shader
,在片元着色器vert
中取得法线并转成rbg
颜色 ,如下:
o.color.xyz = v.normal * 0.5 + 0.5;
o.color.w = 1.0;
完整shader
代码如下:
Shader "Debug/Normals" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// vertex input: position, normal
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed4 color : COLOR;
};
v2f vert (appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex );
o.color.xyz = v.normal * 0.5 + 0.5;
o.color.w = 1.0;
return o;
}
fixed4 frag (v2f i) : SV_Target { return i.color; }
ENDCG
}
}
}
2、摄像机应用shader ,Camera.SetReplacementShader
我们想要全局应用上面的shader
,另写一个c#
脚本,通过Camera
的接口SetReplacementShader
来全局应用shader
。c#
脚本(NormalColor.cs
)代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NormalColor : MonoBehaviour
{
public Shader shader;
private Camera cam;
void Awake()
{
cam = GetComponent<Camera>();
}
void OnEnable()
{
cam.SetReplacementShader(shader, null);
}
void OnDisable()
{
cam.ResetReplacementShader();
}
}
把NormalColor.cs
脚本挂到摄像机上 ,并赋值shader
对象,
3 、运行测试
运行Unity
,测试效果如下 ,是不是别有一番风味~
4 、发挥想象力: 结合粒子系统
不如结合一下粒子效果,创建一个ParticleSystem
,设置它的Renderer
的Mesh
为Cube
,
调节粒子的常规参数,如下,
粒子效果如下:
运行Unity
,显示出法线颜色,效果如下:
六、使用python大法
1、通过python生成法线图
不够尽兴,还能怎么玩呢?我们能不能通过python
生成常规图片的法线图呢?
我在GitHub
上找到了一个 ,GitHub
地址:https://github.com/Mehdi-Antoine/NormalMapGenerator/blob/master/normal_map_generator.py
我做了一些改造 ,方便批量生成,代码见下方,需要安装numpy 、scipp、imageio
。
使用方法:
把需要转换的图片放在input
文件夹中 ,然后执行gen_normalmap.py
脚本即可,
生成结果会放在output
目录中,
效果如下:
gen_normalmap.py
代码如下:
import numpy as np
import scipy.ndimage
import scipy.misc
from scipy import ndimage
import argparse
import imageio
import shutil
import os
input_dir = 'input'
output_dir = 'output'
def smooth_gaussian(im, sigma):
if sigma == 0:
return im
im_smooth = im.astype(float)
kernel_x = np.arange(-3*sigma,3*sigma+1).astype(float)
kernel_x = np.exp((-(kernel_x**2))/(2*(sigma**2)))
im_smooth = scipy.ndimage.convolve(im_smooth, kernel_x[np.newaxis])
im_smooth = scipy.ndimage.convolve(im_smooth, kernel_x[np.newaxis].T)
return im_smooth
def gradient(im_smooth):
gradient_x = im_smooth.astype(float)
gradient_y = im_smooth.astype(float)
kernel = np.arange(-1,2).astype(float)
kernel = - kernel / 2
gradient_x = scipy.ndimage.convolve(gradient_x, kernel[np.newaxis])
gradient_y = scipy.ndimage.convolve(gradient_y, kernel[np.newaxis].T)
return gradient_x,gradient_y
def sobel(im_smooth):
gradient_x = im_smooth.astype(float)
gradient_y = im_smooth.astype(float)
kernel = np.array([[-1,0,1],[-2,0,2],[-1,0,1]])
gradient_x = scipy.ndimage.convolve(gradient_x, kernel)
gradient_y = scipy.ndimage.convolve(gradient_y, kernel.T)
return gradient_x,gradient_y
def compute_normal_map(gradient_x, gradient_y, intensity=1):
width = gradient_x.shape[1]
height = gradient_x.shape[0]
max_x = np.max(gradient_x)
max_y = np.max(gradient_y)
max_value = max_x
if max_y > max_x:
max_value = max_y
normal_map = np.zeros((height, width, 3), dtype=np.float32)
intensity = 1 / intensity
strength = max_value / (max_value * intensity)
normal_map[..., 0] = gradient_x / max_value
normal_map[..., 1] = gradient_y / max_value
normal_map[..., 2] = 1 / strength
norm = np.sqrt(np.power(normal_map[..., 0], 2) + np.power(normal_map[..., 1], 2) + np.power(normal_map[..., 2], 2))
normal_map[..., 0] /= norm
normal_map[..., 1] /= norm
normal_map[..., 2] /= norm
normal_map *= 0.5
normal_map += 0.5
return normal_map
def walk_pic():
for root, dirs, fs in os.walk(input_dir):
for f in fs:
if f.endswith('png') or f.endswith('jpg'):
yield os.path.join(root, f)
def main():
if os.path.exists(output_dir):
shutil.rmtree(output_dir)
os.mkdir(output_dir)
parser = argparse.ArgumentParser(description='Compute normal map of an image')
parser.add_argument('-s', '--smooth', default=0., type=float, help='smooth gaussian blur applied on the image')
parser.add_argument('-it', '--intensity', default=1., type=float, help='intensity of the normal map')
args = parser.parse_args()
sigma = args.smooth
intensity = args.intensity
for input_file in walk_pic():
(_,output_file_name) = os.path.split(input_file)
output_file = output_dir + '/_normal_' + output_file_name
im = np.array(imageio.imread(input_file))
if im.ndim == 3:
im_grey = np.zeros((im.shape[0],im.shape[1])).astype(float)
im_grey = (im[...,0] * 0.3 + im[...,1] * 0.6 + im[...,2] * 0.1)
im = im_grey
im_smooth = smooth_gaussian(im, sigma)
sobel_x, sobel_y = sobel(im_smooth)
normal_map = compute_normal_map(sobel_x, sobel_y, intensity)
imageio.imsave(output_file, normal_map)
if __name__ == "__main__":
main()
2、爬虫爬取二次元美女图片
我有个大胆的想法 ,写个python
爬虫,爬取二次元美女图片,然后…
话不多说 ,上爬虫:
import requests
import os
import urllib
# 百度图片爬虫
class Spider_baidu_image():
def __init__(self):
self.url = 'http://image.baidu.com/search/acjson?'
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.\
3497.81 Safari/537.36'}
self.headers_image = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.\
3497.81 Safari/537.36','Referer':'http://image.baidu.com/search/index?tn=baiduimage&ipn=r&ct=201326592&cl=2&lm=-1&st=-1&fm=result&fr=&sf=1&fmq=1557124645631_R&pv=&ic=&nc=1&z=&hd=1&latest=0©right=0&se=1&showtab=0&fb=0&width=&height=&face=0&istype=2&ie=utf-8&sid=&word=%E8%83%A1%E6%AD%8C'}
# 构造参数数组
def get_param(self):
keyword = urllib.parse.quote(self.keyword)
params = []
for i in range(1,self.paginator+1):
params.append('tn=resultjson_com&ipn=rj&ct=201326592&is=&fp=result&queryWord={}&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=-1&z=&ic=&hd=1&latest=0©right=0&word={}&s=&se=&tab=&width=&height=&face=0&istype=2&qc=&nc=1&fr=&expermode=&force=&cg=star&pn={}&rn=30&gsm=78&1557125391211='.format(keyword,keyword,30*i))
return params
# 构造url数组
def get_urls(self, params):
urls = []
for i in params:
urls.append(self.url+i)
return urls
# 遍历请求url并下载图片
def get_image(self, urls):
cwd = os.getcwd()
file_name = os.path.join(cwd,self.keyword)
if not os.path.exists(self.keyword):
os.mkdir(file_name)
index = 0
for url in urls:
json_data = requests.get(url,headers = self.headers).json()
json_data = json_data.get('data')
for i in json_data:
if i:
image_url = i.get('thumbURL')
index += 1
with open(file_name+'\\{}.jpg'.format(index),'wb') as f:
f.write(requests.get(image_url, headers = self.headers_image).content)
print('下载: ' + image_url)
def __call__(self, *args, **kwargs):
# 构造参数
params = self.get_param()
# 构造url数组
urls = self.get_urls(params)
# 请求
self.get_image(urls)
if __name__ == '__main__':
spider = Spider_baidu_image()
# 关键字
spider.keyword = '二次元美女'
# 页数,每页30张图
spider.paginator = 100
# 开始执行
spider()
爬图片中…
3 、批量生成二次元美女法线图
然后放到gen_normalmap.py
同级目录的input
文件夹中,执行gen_normalmap.py
生对应的法线图 ,
4、猜猜这三个二次元美女是谁
这是其中三张法线图,大家猜得出这三个二次元美女是谁吗~
七、结束语
好了,就先写这么多吧~
喜欢Unity
的同学 ,不要忘记点击关注 ,如果有什么Unity
相关的技术难题,也欢迎留言或私信~
推荐阅读:
- 《Unity+人工智能,让小朋友的画成真 ,六一儿童节一起来画猫猫吧(Unity | 人工智能 | 绘图 | 爬虫 | 猫妖)》
- 《[Unity 3D] 权游红袍女在火中看到了什么,我看到了…(粒子系统 | 火焰特效 | ParticleSystem | 手把手制作)》
- 《[Unity 2D] 重温红白机经典FC游戏,顺便教你快速搭建2D游戏关卡(Tilemap | 场景 | 地图)》
- 《520程序员的浪漫 ,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)》
- 《ShaderGraph使用教程与各种特效案例:Unity2020(持续更新)》
- 《Unity使用ShaderGraph配合粒子系统,制作子弹拖尾特效(Fate/stay night金闪闪的大招效果)》
- 《使用Unity ShaderGraph实现在模型上涂鸦的效果,那么 ,纹个手吧》
- 《学Unity的猫——第十五章:Unity粒子系统ParticleSystem,下雪啦下雪啦》
- 《Unity实现水果忍者切水果的刀痕效果教程(两种实现方式:TrailRenderer 、LineRenderer)》
- 《Unity流体模拟,支持粒子系统 ,支持流体碰撞交互(Obi Fluid插件使用教程)》
- 《玩转贝塞尔曲线,教你在Unity中画Bezier贝塞尔曲线(二阶、三阶),手把手教你推导公式》
- 《Unity UGUI制作雷达图/天赋图/属性图/能力图 ,因为太怕痛就全点了防御力》
- 《使用Unity ShaderGraph实现刮刮乐的刮卡剔除效果 ,感受一下刮中500万的时刻》
八、附录:NormalVisualizerWithGizmo.cs代码
using UnityEngine;
[ExecuteInEditMode]
public class NormalVisualizerWithGizmo : MonoBehaviour
{
#region Enum
public enum Type
{
Vertex,
Surface
}
#endregion Enum
#region Field
public Type drawType = NormalVisualizerWithGizmo.Type.Surface;
[Range(0f, 1)]
public float normalLength = 0.1f;
public Color normalColor = Color.white;
public bool normalColorFromDirection = true;
protected Mesh mesh;
protected new Transform transform;
#endregion Field
#region Method
protected virtual void OnEnable()
{
if (this.mesh == null)
{
MeshFilter meshFilter = base.gameObject.GetComponent<MeshFilter>();
if (meshFilter == null)
{
this.mesh = base.gameObject.GetComponent<SkinnedMeshRenderer>().sharedMesh;
}
else
{
this.mesh = base.gameObject.GetComponent<MeshFilter>().sharedMesh;
}
}
if (this.transform == null)
{
this.transform = base.transform;
}
}
protected virtual void OnDrawGizmos()
{
Color previousColor = Gizmos.color;
Matrix4x4 previousMatrix = Gizmos.matrix;
Gizmos.matrix = Matrix4x4.TRS(this.transform.position,
this.transform.rotation,
this.transform.localScale);
switch (this.drawType)
{
case Type.Surface:
{
DrawSurfaceNormalGizmos();
break;
}
case Type.Vertex:
{
DrawVertexNormalGizmos();
break;
}
}
Gizmos.color = previousColor;
Gizmos.matrix = previousMatrix;
}
protected virtual void DrawVertexNormalGizmos()
{
Vector3 [] vertices = this.mesh.vertices;
Vector3 [] normals = this.mesh.normals;
Vector3 normal;
Gizmos.color = this.normalColor;
for (int i = 0; i< this.mesh.vertexCount; i++)
{
normal = Vector3.Normalize(normals[i]);
if (this.normalColorFromDirection)
{
Gizmos.color = new Color(normal.x, normal.y, normal.z);
}
Gizmos.DrawRay(vertices[i], normal * this.normalLength);
}
}
protected virtual void DrawSurfaceNormalGizmos()
{
Vector3[] vertices = this.mesh.vertices;
Vector3[] normals = this.mesh.normals;
int[] triangles = this.mesh.triangles;
Vector3 normal;
Vector3 position;
int triangleIndex0;
int triangleIndex1;
int triangleIndex2;
Gizmos.color = this.normalColor;
for (int i = 0; i <= triangles.Length - 3; i += 3)
{
triangleIndex0 = triangles[i];
triangleIndex1 = triangles[i + 1];
triangleIndex2 = triangles[i + 2];
position = (vertices[triangleIndex0]
+ vertices[triangleIndex1]
+ vertices[triangleIndex2]) / 3;
normal = (normals[triangleIndex0]
+ normals[triangleIndex1]
+ normals[triangleIndex2]) / 3;
normal = Vector3.Normalize(normal);
if (this.normalColorFromDirection)
{
Gizmos.color = new Color(normal.x, normal.y, normal.z);
}
Gizmos.DrawRay(position, normal * this.normalLength);
}
}
#endregion Method
}
本文版权归去快排wWw.seogUrublog.com 所有,如有转发请注明来出,竞价开户托管,seo优化请联系qq❉61910465