当前位置: 首页 > news >正文

unity 位移贴图正弦波面

本文的目标是制作一个虽时间波动的正弦波面,效果如下图

 首先在unity中创建一个平面,这没什么可说的。

曲面细分

unity中默认的plane面数是很少的,不足以形成一个光滑的波面,所以第一步是进行曲面细分。

 如图所示,我们需要先输入原始的顶点数据,unity中会将他写成vertex shader的形式,但是实际上的vertex shader在后面。

在hull program和domain program中我们可以设定细分的规则。曲面细分的具体原理在此不赘述,会用即可。需要注意的是在hullfun中的参数决定了细分的程度,这里我们直接为它赋值,更科学的方式可能是根据镜头的距离设定它的值。

TessellationFactors hullFun (InputPatch<tessVertexData,3> v) {
	TessellationFactors o;
	o.edge[0] = _TessellationUniform;//设定的参数
	o.edge[1] = _TessellationUniform;
	o.edge[2] = _TessellationUniform;
	o.inside = _TessellationUniform;
	return o;
}

[UNITY_domain("tri")] 表示适用于三角形,还有quad(四边形)
[UNITY_outputcontrolpoints(3)]//输出的控制点数量
[UNITY_outputtopology("triangle_cw")]//输出拓扑结构为顺时针三角形,还有triangle_ccw(逆时针三角形)、line(线段)
[UNITY_partitioning("fractional_odd")]//分数分割模式,还有integer(整数模式)
[UNITY_patchconstantfunc("hullFun")]//细分函数
            //hull着色器:定义细分规则
tessVertexData hul (InputPatch<tessVertexData,3> v, uint id : SV_OutputControlPointID) {
			    return v[id];
}

[UNITY_domain("tri")]
//domain着色器:计算细分后的顶点位置和数据,同时执行顶点着色器
v2g dom (TessellationFactors tessFactors, const OutputPatch<tessVertexData,3> vi, float3 bary : SV_DomainLocation) {
			    vertexData v;
			    v.vertex = vi[0].vertex*bary.x + vi[1].vertex*bary.y + vi[2].vertex*bary.z;
			    v.tangent = vi[0].tangent*bary.x + vi[1].tangent*bary.y + vi[2].tangent*bary.z;
			    v.normal = vi[0].normal*bary.x + vi[1].normal*bary.y + vi[2].normal*bary.z;
			    v.uv = vi[0].uv*bary.x + vi[1].uv*bary.y + vi[2].uv*bary.z;
                return vert (v);
}

为了清晰地看到网格,我们将网格以线条的方式渲染出来,渲染的原理是用几何着色器计算出每个三角网格的重心,根据三角形的重心坐标到三顶点的最小距离插值颜色。

            [maxvertexcount(3)]
            //几何着色器
            void geo (
	            triangle v2g v[3],
	            inout TriangleStream<g2f> tStream
            ) {
                float4 barycenter = (v[0].vertex + v[1].vertex + v[2].vertex)/3;
                float3 normal = (v[0].normal + v[1].normal + v[2].normal)/3;

	            v[0].normal = normal;
	            v[1].normal = normal;
	            v[2].normal = normal;

	            g2f g0, g1, g2;
	            g0.data = v[0];
	            g1.data = v[1];
	            g2.data = v[2];

                //
	            g0.barycentricCoordinates = float3(0, 0, 1);
	            g1.barycentricCoordinates = float3(0, 1, 0);
	            g2.barycentricCoordinates = float3(1, 0, 0);

	            tStream.Append(g0);
	            tStream.Append(g1);
	            tStream.Append(g2);
                tStream.RestartStrip();
            }


			fixed4 frag (g2f i) : SV_Target
			{
                fixed4 col = tex2D(_MainTex, i.data.uv);
                float3 barys = i.barycentricCoordinates;
	            float3 deltas = fwidth(barys);
	            float3 smoothing = deltas * _WireframeSmoothing;
	            float3 thickness = deltas * _WireframeThickness;
	            barys = smoothstep(thickness, thickness + smoothing, barys);
	            float minBary = min(barys.x, min(barys.y, barys.z));

	            return float4(lerp(_WireframeColor, col, minBary),1);//
				return col;
			}
			ENDCG
		} 

以上部分参考了b站lyh萌主的文章https://www.bilibili.com/read/cv16290237

计算着色器

如标题所说,我们要用到一个随时间变化的位移贴图,显然它需要实时生成。可以选择在脚本的update函数中每次创建新的纹理,然后一个一个像素填充它。如果你这样做会发现帧率非常低,尤其当CPU性能不强时,因为CPU不擅长做这种并行计算。所以我们把这个任务交给GPU,这就用到了compute shader。

虽然都是shader,但是unity中compute shader是用以 DirectX 11 样式 HLSL 语言编写的,所以与其他shader写法有所不同,也不在常规渲染管线中。

下图是默认的compute shader,它包含了最重要的几部分。在这里插入图片描述

#pragma kernel CSMain这一句可以理解为声明了CS中的某个函数(函数名为CSMain)。

RWTexture2D<float4> Result;RW其实是Read和Write的意思,Texture2D就是二维纹理,因此它的意思就是一个可以被Compute Shader读写的二维纹理。CS是运行在GPU上的程序,并且独立于渲染管线,所以需要一个载体承担输入或输出。做渲染用途时,我们一般就将一个纹理作为载体。

一般我们shader通常是只读的,大多使用的是sampler2D,然后通过tex2D函数已经UV坐标访问,但RWTexture2D的访问是直接通过Result[uint2(0,0)]来访问,值为float4型。

由于这个纹理是需要读和写的,所以需要使用render texture,而不能是Texture2D。它的创建方式如下,注意需要开启它的读写,并调用create方法。

public RenderTexture Displace;
...
    void Start()
    {
        Displace = new RenderTexture(1024, 1024,0, RenderTextureFormat.ARGBFloat);
        Displace.enableRandomWrite = true;
        Displace.Create();
        ...
    }

 [numthreads(8,8,1)]表示一个线程组的线程数量,即8*8*1,线程组的设定会影响计算效率,不过具体我也不太懂,有大佬懂得希望不吝赐教。在这我们让它保持默认。

最后是核函数,重要的是它可以有几个输入的参数

SV_GroupID:线程组的id
SV_GroupIndex:即在每一个线程组元素里,线程的索引,[numthreads(8,8,1)],则索引范围(0, 0, 0) - (8, 8, 0),
SV_DispatchThreadID:这个就是全局唯一的id,可以理解为一张图片的每个像素坐标

所以上图中id.x id.y分别代表纹理上某个像素的横纵坐标。

写完了一个CS,接下来就是如何使用它。

public class createTexture : MonoBehaviour
{
    public ComputeShader cshader;
    private int kernelHandle;

    ...

    void Start()
    {
        ...
        kernelHandle = cshader.FindKernel("CSMain");
    }

    void Update()
    {
        cshader.SetTexture(kernelHandle, "Result", Displace);
        cshader.Dispatch(kernelHandle, 1024 / 8, 1024 / 8, 1);
        ...
    }
}

我们需要定义一个compute shader,并为它的核函数定义一个索引(int类型)

将一个render texture传入作为RWTexture2D。

最后根据我们传入的texture大小(1024*1024)调用dispatch方法启动运算

这部分参考了博文

下面是实际用到的CS,我将位移量保存在纹理的R通道中,注意其中的常量PI最好使用宏定义

#pragma kernel CSMain
#define PI 3.14159274f

RWTexture2D<float4> Result;

float time;
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    Result[id.xy] = float4((cos(id.x/ 1024.0f * 2 * PI*5+time) + cos(id.y/1024.0f * 2 * PI*5+time)) * 0.25f + 0.5f, 0, 0, 1);
}

位移贴图

最后就是把位移贴图应用到渲染中。

将compute shader计算好的texture传入shader,命名为_DisplaceTex,读出它的R通道,计算位移并加在法线方向上。

注意这里对纹理的采样必须用tex2Dlod(),因为tex2D不能用在顶点着色器里(不知原因)。

		v2g vert(vertexData v)
		{
			...
			float d = pow( tex2Dlod(_DisplaceTex, float4(v.uv.xy, 0, 0)).r, _Power) * _Displacement;
			v.vertex.xyz += v.normal * d;
            ...
		}

相关文章:

  • 新能源汽车 BMS 学习笔记篇——如何选择继电器 MOS 管作为开关
  • MacBook上怎么查找历史复制记录?
  • Go语言 管道1
  • Beyond Homophily Reconstructing Structure for Graph-agnostic Clustering
  • Python | Leetcode Python题解之第394题字符串解码
  • 在 CentOS 中永久关闭防火墙的步骤
  • 普中51单片机学习(8*8LED点阵)
  • Docker - compose常用命令(常规操作顺序)
  • 自然语言处理: 第十三章Xinference部署
  • vue页面菜单权限问题解决
  • 数据仓库和数据湖的区别
  • 2024-02-26(金融AI行业概览与大数据生态圈)
  • javaScript 防抖/节流,探索学习,对新手友好的内容
  • Servlet基础(1)
  • 【Svelte】-(4)If 条件判断语句 / Each 循环语句 / Await 异步处理块
  • 手把手实现一个进度条时钟,麻麻再也不用担心我把时间看茬了
  • java毕业设计潮购购物网站Mybatis+系统+数据库+调试部署
  • Verilog写状态机的三种描述方式之二段式
  • 【正点原子I.MX6U-MINI应用篇】1、编写第一个应用App程序helloworld
  • 【Vue】父子组件通信
  • AI 应用的全流程存储加速方案技术解析和实践分享
  • 云服务器的介绍
  • 《大数据分析技术》课程设计
  • unity urp 实现衣服上面片的效果
  • Spring Boot核心之基本配置、日志配置、自动配置、条件注解
  • 智能手术机器人起源及应用(一)
  • 20分钟学会git基本操作,创建远程仓库
  • keepalived实现nginx负载均衡机高可用
  • STL常用容器——stack容器的使用
  • 基于Dijkstra、A*和动态规划的移动机器人路径规划(Matlab代码实现)
  • 【C语言】文件操作(万字详解,教你掌握文件操作)
  • 【数据结构】-----二叉树(递归、层次实现二叉树的遍历)