先上效果链接:
https://www.bilibili.com/video/av75962636/

之前在做动画的时候见到有人用Krakatoa粒子做过类似的效果,其中有些方法和技巧很值得借鉴,所以我就在Unity里尝试着制作了。

制作这个效果主要用到了ComputeShader、ComputeBuffer、GeometryShader,在试验的过程中也用了Unity的JobSystem来提高效率,但还是比ComputeShader效率差了不止一个数量级。

这个效果有个最大的难点是粒子需要构成汽车,还要还原汽车的形状、光照、反射甚至玻璃的透明,更要在相机移动的时候需要粒子的光照和反射和透明也一起变,最后还要在保证高帧率的情况下显示和模拟千万级数量的粒子。看起来无从下手,我们一步步来。

先是准备工作,祭出我的御用玛莎拉蒂:

可以简单调一下材质效果。这里给大家分享个简单出效果的制作车漆材质的办法:车漆重要的是虫漆效果,也就是有个底层材质外加个光滑表面材质,所以可以在Standard Surface Shader上新建个透明Pass专门负责光滑表面反射。由于车漆不是本文重点就不赘述了。下面进入干货区。

我用的硬件是I7 8750H + gtx1070的笔记本电脑。

一、实时渲染千万级的粒子

如何能够实时渲染千万级数量的粒子?这是首先要解决的问题和其他效果的前提。我通常的做法是用Graphics.DrawProcedural先画点,然后在GeometryShader中给每个点周围画个四边形(可以在屏幕空间构建,这样可以保证四边形符合实际像素的尺寸)。
GeometryShader如下,_Width是四边形的宽度:

half _Width;
[maxvertexcount(4)]
void geom(point v2g input[1], inout TriangleStream<g2f> triStream)
{v2g p = input[0];float offsetX = _Width / _ScreenParams.x * p.position.w * 0.5f;float offsetY = offsetX;float aspectReciprocal = _ScreenParams.y / _ScreenParams.x;offsetX *= aspectReciprocal;g2f pIn = (g2f)0;pIn.vertex = p.position + float4(-offsetX, offsetY, 0, 0);pIn.uv = half2(0, 1);triStream.Append(pIn);pIn.vertex = p.position + float4(offsetX, offsetY, 0, 0);pIn.uv = half2(1, 1);triStream.Append(pIn);pIn.vertex = p.position + float4(-offsetX, -offsetY, 0, 0);pIn.uv = half2(0, 0);triStream.Append(pIn);pIn.vertex = p.position + float4(offsetX, -offsetY, 0, 0);pIn.uv = half2(1, 0);triStream.Append(pIn);
}

c#代码如下,其中_TotalPointCount就是点的个数:

private void OnRenderObject()
{if (Camera.current == Camera.main){_Material.SetPass(0);Graphics.DrawProcedural(MeshTopology.Points, _TotalPointCount);}
}

二、用粒子填充汽车的形状

其实思路很简单,就是随机找个网格的三角形,在这个三角形上随机找个位置点然后放个粒子。
那么问题来了:每个三角形大小不一样,需要保证面积大的三角形能覆盖的粒子得多一些这样才能均匀的构建粒子汽车?就算找到三角形之后,如何在三角形上随机找个点?就算这两点都解决了,效率怎么保证?千万数量级的粒子不是说它出来就能出来的,虽然不用实时计算(第一次算出来后面就直接用),然是也不能让人等到花也谢了吧。

第一个问题,既然跟面积有关那么我们完全可以用面积作为随机计算的一个因素。首先算出每个三角形的面积,然后把面积按照一维数组排成一排。再算出所有三角形面积总和,每次随机取样的时候取 0——三角形总面积,然后根据取到的面积的随机值回到面积数组里找序号,再用序号找到三角形。
首先缓存三角形为方便后续计算,定义个结构体记录需要的数据:

public struct MeshCache
{public Vector3[] positions;//所有顶点位置public int[] triangles;//所有三角形序号public float[] triangleAreas;//所有三角形面积数组。这里为了方便通过面积找到序号,数组里面每个值都是前面所有三角形面积的和public int triangleAreaCount;//三角形面积数组的长度public float totalArea;//总三角形面积/// <summary>/// 根据面积找到序号/// </summary>/// <param name="area">随机生成的面积值</param>/// <returns></returns>public int GetTriangleAreaIndex(float area){for (int i = 0; i < triangleAreaCount; i++){if (triangleAreas[i] >= area){return i;}}return -1;}
}

再计算面积生成缓存数据:

/// <summary>
/// 生成网格缓存
/// </summary>
private void GenerateMeshCache()
{var meshFilters = _TargetMeshGo.GetComponentsInChildren<MeshFilter>();int prevVertCount = 0;var totalPositions = new List<Vector3>();var totalTriangles = new List<int>();var totalTriangleAreas = new List<float>();foreach (var mfItem in meshFilters){var itemVertices = mfItem.sharedMesh.vertices;var localToWorld = mfItem.transform.localToWorldMatrix;for (int i = 0; i < itemVertices.Length; i++){itemVertices[i] = localToWorld.MultiplyPoint(itemVertices[i]);}totalPositions.AddRange(itemVertices);var itemTriangles = mfItem.sharedMesh.triangles;for (int i = 0; i < itemTriangles.Length; i++){itemTriangles[i] += prevVertCount;}totalTriangles.AddRange(itemTriangles);prevVertCount += itemVertices.Length;}float totalArea = 0;for (int i = 0, count = totalTriangles.Count; i < count; i += 3){var area = GetTriangleArea(totalPositions[totalTriangles[i]], totalPositions[totalTriangles[i + 1]], totalPositions[totalTriangles[i + 2]]);totalArea += area;totalTriangleAreas.Add(totalArea);}_MeshCache.positions = totalPositions.ToArray();_MeshCache.triangles = totalTriangles.ToArray();_MeshCache.triangleAreas = totalTriangleAreas.ToArray();_MeshCache.totalArea = totalArea;_MeshCache.triangleAreaCount = _MeshCache.triangleAreas.Length;
}

可能有人会问,三角形面积怎么计算?根据三角形的面积计算公式底x高/2,确定一条边作为底,需要算出高。
这里给出两种算出三角形的高的方法,一是根据向量余弦,二是根据向量投影(测试下来第一种方法快)。代码如下:

private float GetTriangleArea(Vector3 A, Vector3 B, Vector3 C)
{if (A == B || B == C || A == C) return 0;var vectorAB = B - A;var vectorAC = C - A;var dot = Vector3.Dot(vectorAB.normalized, vectorAC.normalized);var angle = Mathf.Acos(dot);var height = vectorAB.magnitude * Mathf.Sin(angle);var width = vectorAC.magnitude;return width * height * 0.5f;
}
private float GetTriangleArea2(Vector3 A, Vector3 B, Vector3 C)
{if (A == B || B == C || A == C) return 0;var vectorAB = B - A;var vectorAC = C - A;var vectorACNormalized = vectorAC.normalized;var vectorZ = Vector3.Cross(vectorAB.normalized, vectorACNormalized).normalized;var vectorHeight = Vector3.Cross(vectorACNormalized, vectorZ).normalized;var height = Vector3.Dot(vectorAB, vectorHeight);var width = vectorAC.magnitude;return width * height * 0.5f;
}

第二个问题,三角形上随机找点,这里我偷了个懒,感觉这种计算肯定有人做过所以我就去搜索了一下,果然找到了(原地址在注释里有写):

/// <summary>
/// reference from https://stackoverflow.com/questions/4778147/sample-random-point-in-triangle
/// </summary>
private Vector3 GetRandomPointOnTriangle(Vector3 A, Vector3 B, Vector3 C)
{var r1 = Random.Range(0.0f, 1.0f);var r2 = Random.Range(0.0f, 1.0f);var result = (1.0f - Mathf.Sqrt(r1)) * A + (Mathf.Sqrt(r1) * (1.0f - r2)) * B + (Mathf.Sqrt(r1) * r2) * C;return result;
}

然后收集生成的随机点,_PointsOnMesh是存储随机点的数组:

for (int i = 0; i < _TotalPointCount; i++)
{var random = Random.Range(0.0f, _MeshCache.totalArea);var triangleAreaIndex = _MeshCache.GetTriangleAreaIndex(random);if (triangleAreaIndex != -1){var triangleStartIndex = triangleAreaIndex * 3;var point = GetRandomPointOnTriangle(_MeshCache.positions[_MeshCache.triangles[triangleStartIndex]], _MeshCache.positions[_MeshCache.triangles[triangleStartIndex + 1]], _MeshCache.positions[_MeshCache.triangles[triangleStartIndex + 2]]);_PointsOnMesh[i] = point;}else{Debug.LogError("triangleAreaIndex not found! area:" + random);}
}

第三个问题,计算优化。这辆车模型大概30多万个三角形,计算面积消耗了0.23秒,因为只需要计算一次所以还能接受。但是计算分布随机点的时候用了30秒(50000个粒子),这就让人不能接受了,因为这点粒子根本不能满足要求,根本不能填出个车的形状:

你能看出这是车吗?好吧有点像,但是根本不符合我的使用要求。我需要的是至少10000000个粒子,按照这个计算量需要将近2小时的计算时间,那我还怎么测试啊,每次点击Play需要等2小时???所以必须优化这个计算过程。

我首先想到的就是Unity的JobSystem,可以用多核心同时计算,所以用IJobParallelFor来试了一下,但是测试结果让我大跌眼镜——同样是随机分布50000个粒子居然用了28秒,只快了2秒?可能是内存分配消耗了太多时间了?一时半会找不到原因,我不得不尝试其他方法,ComputeShader是个可以尝试的方向。

还好这个计算过程比较简单,很快就把代码写成了ComputeShader的版本,如下:

#pragma kernel DistributionStructuredBuffer<float3> positions;
StructuredBuffer<int> triangles;
StructuredBuffer<float> triangleAreas;
int triangleAreasCount;
float totalArea;
int totalPointCount;StructuredBuffer<float> areaRandoms;
StructuredBuffer<float> r1Randoms;
StructuredBuffer<float> r2Randoms;RWStructuredBuffer<float3> resultPoints;int GetTriangleAreaIndex(float area)
{for (int i = 0; i < triangleAreasCount; i++){if (triangleAreas[i] >= area){return i;}}return -1;
}
float3 GetRandomPointOnTriangle(float3 A, float3 B, float3 C, int i)
{float r1 = r1Randoms[i];float r2 = r2Randoms[i];float3 result = (1.0f - sqrt(r1)) * A + (sqrt(r1) * (1.0f - r2)) * B + (sqrt(r1) * r2) * C;return result;
}
[numthreads(1024, 1, 1)]
void Distribution(uint3 id : SV_DispatchThreadID)
{int index = (int)id.x;if (index < totalPointCount){float random = areaRandoms[index];int triangleAreaIndex = GetTriangleAreaIndex(random);if (triangleAreaIndex != -1){int triangleStartIndex = triangleAreaIndex * 3;float3 resultPoint = GetRandomPointOnTriangle(positions[triangles[triangleStartIndex]], positions[triangles[triangleStartIndex + 1]], positions[triangles[triangleStartIndex + 2]], index);resultPoints[index] = resultPoint;}}
}

其中需要注意的是Random,由于在GPU中并没有现成的Random函数,所以我是在CPU中用Random生成了数组给GPU查表来用。C#代码如下:

const int interval = 100000;
int loopCount = (_TotalPointCount / interval) + ((_TotalPointCount % interval) > 0 ? 1 : 0);
int calculatedPointCount = 0;
for (int loopIndex = 0; loopIndex < loopCount; loopIndex++)
{int pointCount = (_TotalPointCount - calculatedPointCount > interval) ? interval : _TotalPointCount - calculatedPointCount;_PositionsCB = new ComputeBuffer(_MeshCache.positions.Length, 12);_PositionsCB.SetData(_MeshCache.positions);_TrianglesCB = new ComputeBuffer(_MeshCache.triangles.Length, 4);_TrianglesCB.SetData(_MeshCache.triangles);_TriangleAreasCB = new ComputeBuffer(_MeshCache.triangleAreas.Length, 4);_TriangleAreasCB.SetData(_MeshCache.triangleAreas);var areaRandoms = new float[pointCount];var r1Randoms = new float[pointCount];var r2Randoms = new float[pointCount];for (int i = 0; i < pointCount; i++){areaRandoms[i] = Random.Range(0.0f, _MeshCache.totalArea);r1Randoms[i] = Random.Range(0.0f, 1.0f);r2Randoms[i] = Random.Range(0.0f, 1.0f);}_AreaRandomsCB = new ComputeBuffer(areaRandoms.Length, 4);_AreaRandomsCB.SetData(areaRandoms);_R1RandomsCB = new ComputeBuffer(r1Randoms.Length, 4);_R1RandomsCB.SetData(r1Randoms);_R2RandomsCB = new ComputeBuffer(r2Randoms.Length, 4);_R2RandomsCB.SetData(r2Randoms);_KernelID_Distribution = _DistributionComputeShader.FindKernel("Distribution");_DistributionComputeShader.SetBuffer(_KernelID_Distribution, "positions", _PositionsCB);_DistributionComputeShader.SetBuffer(_KernelID_Distribution, "triangles", _TrianglesCB);_DistributionComputeShader.SetBuffer(_KernelID_Distribution, "triangleAreas", _TriangleAreasCB);_DistributionComputeShader.SetBuffer(_KernelID_Distribution, "areaRandoms", _AreaRandomsCB);_DistributionComputeShader.SetBuffer(_KernelID_Distribution, "r1Randoms", _R1RandomsCB);_DistributionComputeShader.SetBuffer(_KernelID_Distribution, "r2Randoms", _R2RandomsCB);var intervalResultCB = new ComputeBuffer(pointCount, 12);var intervalResult = new Vector3[pointCount];intervalResultCB.SetData(intervalResult);_DistributionComputeShader.SetBuffer(_KernelID_Distribution, "resultPoints", intervalResultCB);_DistributionComputeShader.SetInt("triangleAreasCount", _MeshCache.triangleAreaCount);_DistributionComputeShader.SetFloat("totalArea", _MeshCache.totalArea);_DistributionComputeShader.SetInt("totalPointCount", pointCount);_DistributionComputeShader.Dispatch(_KernelID_Distribution, pointCount / 1024 + 1, 1, 1);intervalResultCB.GetData(_PointsOnMesh, calculatedPointCount, 0, pointCount);intervalResultCB.Release();_PositionsCB.Release();_TrianglesCB.Release();_TriangleAreasCB.Release();_AreaRandomsCB.Release();_R1RandomsCB.Release();_R2RandomsCB.Release();calculatedPointCount += interval;
}

由于需要传递很多的数据所以用了好几个ComputeBuffer。可以看到我对每次计算做了100000个粒子的数量限制,循环多次计算把数据再拷贝回结果数组中,这样做是因为粒子到一定的数量之后GPU计算貌似就假死了,也不清楚什么原因,就这样绕过去了。测试一下,果然快很多,50000个粒子只需要0.15秒!!!直接上10000000个粒子,25秒计算完成!!!不错了,可以用了,看下面截图,已经填得满满的了:

为了下一步的测试方便,我把粒子保存为文件,每次就可以在1秒之内从磁盘读取(一千万个粒子保存为117mb的文件)。

三、还原汽车的光照和反射

这里要用到一个技巧。光照和反射还有透明这些效果并不是在粒子的材质上计算的,如果是相机是静态的只计算一次也还行,但是相机是可以自由移动的,这个时候还需要有光照和反射和透明,重点来了,可以用相机渲染一张RenderTexture,然后构成汽车的粒子按照屏幕空间的uv坐标来采样RenderTexture显示颜色。 这种方法不允许相机离得太近,但是完全符合我的需求了。下图就是相机离得太近之后:

这里没什么技术含量,在VertexShader或GeometryShader里可以计算屏幕uv,我在GeometryShader里加了一行:

四、扩散粒子

这里对于熟悉ComputeShader的同学就没啥技术可讲了,其实就是用个Noise的函数(Google一下能找到cg的版本),根据粒子的空间位置生成个向量值作为力,调整为合适的大小和缩放,再用力影响粒子的加速度和速度和位置,最终效果就出来了:

我的示例里用个平面拖动来触发扩散效果,其实只是判断了一下世界坐标的z轴位置。

当然这个效果还有很多不足的地方,比如粒子散开之后再旋转相机会让散开的粒子的颜色变化太快出现闪的的情况,但是也是有办法优化,今天就不多写了。

下次在UE4里试试看能不能做出这个效果。

炫酷的汽车幻化为粒子效果分享相关推荐

  1. 炫酷的汽车换色效果分享——X战警魔形女变身

    炫酷的汽车换色效果--X战警魔形女 原理: 1.使用ComputeShader在汽车网格上散布很多随机点,记下这些点的位置和法线. 2.使用DrawMeshInstancedIndirect在上面计算 ...

  2. 的图片怎么循环渲染_十分钟教你做个炫酷的图片切换过度效果

    做个炫酷的图片切换过度效果 首先,今天是520节日.到了520这类为情侣准备的节日,小编都会感到一万点暴击-- 首先酸一波,搞点事情(蹭波热度). 给大家分享一个520特效页面:看完记得回来为小编点个 ...

  3. 【每日一练】109—一款炫酷按钮的鼠标悬停效果

    文 | 杨小爱 写在前面 按钮,几乎是任何一个项目都会用到的一个组件,因此,今天,我们来练习一个好玩的鼠标效果,具体效果,请看下面的GIF截图: 我们看完了最终效果,现在,我们一起来看一下它的源码. ...

  4. jquery 实现智能炫酷的翻页相册效果

    jquery 实现智能炫酷的翻页相册效果 巧妙的运用 Html 的文档属性,大大减少jquery 的代码量,实现了智能炫酷的翻页相册.兼容性很好,实现了代码与标签的完全分离 ​1. [代码]jquer ...

  5. 【CSS3】多款炫酷鼠标悬停图文动画效果

    演示效果: HTML代码如下: <!doctype html> <html lang="zh"> <head><meta charset= ...

  6. 炫酷canvas网页背景动画效果

    下载地址非常炫酷的网页背景旋转特效,基于canvas画布实现的网页背景动画效果 dd:

  7. js制作的炫酷3D太阳系行星运行效果

    想象着打开网页就能浏览太阳系行星的运行情况,促进我们更好的了解这个宇宙星空,于是找到了这样一段代码可以完美的实现这个功能,通过css和js就可以实现在网页上展示一个完美的太阳系行星的运行情况,效果炫酷 ...

  8. 炫酷的VS Code毛玻璃效果

    VS Code 开启毛玻璃效果以及霓虹主题 效果: 必要插件 1.Custom CSS and JS Loader 首先安装插件:Custom CSS and JS Loader 用于自定义CSS和J ...

  9. mapv结合百度地图treejs实现炫酷三维大数据可视化效果

    随着大数据.云计算.物联网的诞生.大量的设备数据.传感器数据.行为数据.日志数据.基础画像数据.运行数据等等都对传统的数据展现提出了新的要求 .随着前端技术的不断成熟,客户对业务系统的要求也由原来的简 ...

最新文章

  1. echart饼图标签重叠_Echarts 解决饼图文字过长重叠的问题
  2. Bug之本地可以发送邮件 测试服不行
  3. php redis 读写分离类,yii实现redis读写分离
  4. angular示例_Angular Dependency Injection用示例解释
  5. selenium获取接口 HAR
  6. 2018年Android面试题整理
  7. 话里话外:从纯技术顾问到业务咨询顾问的能力发展路径(上)
  8. 翻译www.djangobook.com之第一章:Django介绍
  9. C. Balanced Stone Heaps
  10. 秋招公司真题刷题2019-2020java工程师
  11. salve mysql_mysql:master--salve主从库同步备份锁表操作
  12. pdf和图像文字识别提取工具
  13. 前端三刺客---JS(基础语法)
  14. Ubuntu 命令大全 Ubuntu技巧 (转)
  15. 简单介绍pytorch中分布式训练DDP使用 (结合实例,快速入门)
  16. 计算机网络协议分析全知识点总结兼期末复习重点
  17. 学习Python,经常见到PEP,那么PEP是什么呢?
  18. GBase 8s 因更换网络导致的908错误
  19. 互联网快讯:华为推“矿鸿”; 京东MALL开业;掌门教育、猿辅导布局素质教育
  20. 暴光史上最强的女生勾引男生的方法

热门文章

  1. 独秀日记:如果网购服装都很合身,那逛街无聊多了
  2. 20个值得一读的设计博客
  3. 躺着赚钱的方法是什么?这样做可以提升“被动收入”!
  4. 回顾javaScript的面向对象继承
  5. centos7.x openvpn+freeradius认证daloradius管理
  6. C++学习过程中踩的地雷
  7. UE4 C++ 一个Character踩地雷
  8. 基本数据类型和String相加结果一定是字符串型
  9. Calendar类的get () 与set()在获取月份情况下与设置月份情况下不同
  10. 京东年货节,如何一键群发营销短信?