Unity was once heralded as the savior of the video game industry. It was relatively easy to use, and provided an engine/framework for multiple games rather than just one. Even as late as the early 2000s, some game companies, especially in Japan, weren’t even sharing similar engines within their own teams! Before the popularity of commercial game engines, each game was built bespoke, which had some advantages, but took a lot of time, and made each port a chore. One of the greatest things Unity provided was a relatively easy pipeline to console releases, making ports more about platform quirks than full recoding.

I remember countless interviews I did as a journalist in 2005 where the interviewee said “now that there’s engines like Unity, things are getting easier.” My game company Necrosoft has used Unity for every commercial project it has ever made.

But now I can say, unequivocally, if you’re starting a new game project, do not use Unity. If you started a project 4 months ago, it’s worth switching to something else. Unity is quite simply not a company to be trusted.

What has happened? Across the last few years, as John Riccitiello has taken over the company, the engine has made a steady decline into bizarre business models surrounding an engine with unmaintained features and erratic stability.

I’ll talk about eroding features first. Unity has internal champions for its features. Once those champions leave the company, that feature languishes and falls apart. Unity buys competing products, and then if the owner of that product leaves the company, it is no longer supported. Unity has a “stable” version of its product called the “LTS” version. More experimental features are pushed to a beta which developers can use if they’re curious. Currently we are on an LTS version that requires us to open a blank page before doing anything else, otherwise the engine simply crashes. This is because of an error Unity introduced recently which they have not fixed. It adds a couple minutes every time we open the project, and is anything but stable.

The latest and final straw is this announcement, which you may have seen some developer friends talking about.

I’ll break down some of the most important factors in this discussion:

  • Unity personal, which is free, now cannot be used offline.
  • all tiers of unity now require developers to pay a set fee of a few cents for every game that’s installed
  • Unity plus (which is going away), and pro, and enterprise levels all cost a subscription, and most professional developers have to use it.
  • different tiers of downloads and income determine how much you pay.
  • Unity has never made money off subscriptions, it has always made money off its ads platform (which you see in f2p mobile games, etc)
  • There’s a discount if you use Unity Services.
  • This money is meant to help with runtime issues when they can’t even get their base stable version to run without crashing.

So the problem becomes this: they are already charging a subscription, and now a per install cost on top of that. What’s the point of the subscription if we’re also paying another charge on top of that? Why wouldn’t it be free at that point?

Also, it’s on developers to sort through these two types of costs, meaning Unity has added a bunch of admin work for us, while making it extremely costly for games like Vampire Survivor to sell their game at the price they do. Vampire Survivor’s edge was their price, now doing something like that is completely unfeasible. Imagine releasing a game for 99 cents under the personal plan, where Steam takes 30% off the top for their platform fee, and then unity takes 20 cents per install, and now you’re making a maximum of 46 cents on the dollar. As a developer who starts a game under the personal plan, because you’re not sure how well it’ll do, you’re punished, astoundingly so, for being a breakout success. Not to mention that sales will now be more costly for developers since Unity is not asking for a percentage, but a set fee. If I reduce the price of my game, the price unity asks for doesn’t decrease.

This all comes out of the developer’s pocket, but publishers won’t want to be on the hook for it either – expect fewer games to be pitched with Unity going forward.

And NOW consider bundles. Each install or activation counts as something that must be paid. We did the itch.io bundle for ukraine. That was approximately a million games given out per developer. If all those were installed, developers would owe $200,000 just for having given their game to charity. It makes charity bundles completely unfeasible. We have 800,000 copies of our game Gunhouse that are owned but not yet installed. Will we be on the hook for thousands of dollars for a game we donated to charity?

[edit: they have since walked back the idea that charity bundles would be on the hook for this, but there’s no method of tracking which games came from charity bundles and which simply came from that platform through purchases, so this is bogus. I have it on good authority that they just today learned what charity bundles are and how they work.]

On top of this there is no minimum cooldown period after they change their fee, so they can raise it as they like, whenever they like. Effectively we’re all locked into an upward-only, per-install pricing model that can change whenever Unity decides they need extra revenue to make their stock look healthy. No developer would have decided to use Unity if this was the business model from the start.
It proves that they’re willing to completely change things up with no notice. January is barely 4 months away, and this decision will affect titles that have been in development for years, and haven’t factored this into their budgets. If we had the option, we’d change now, but we’re too far into development. Unreal Engine, to their credit, only holds you to the EULA you signed up for, not whatever they’ve decided most recently.

I want to double down on this point – we did not sign up for this. Frankly the Unity Plus tier, which we currently use, which is going away presently, wasn’t even around when we signed up. Oh, and did I mention we’re automatically being switched to the more expensive Pro from Plus if we don’t cancel our subscription? If the agreement changes underneath you as you’re making the game, you can’t budget for it, and trust is completely lost. We did not plan for this, and it screws us massively on Demonschool, which is tracking to be our most successful game. You might say poor you, but again, we did not sign up for this and have no option to say no, since we’re close to release and this change is 4 months out. You can’t simply remake an entire game in another engine when you’ve been working on it for 4+ years.

It is clear that Riccitiello never considered that people might make games for reasons other than money. At least the attitude is consistent.

There is one other critical element here – developers are being offered a reduced price if they use Unity Services. This is basically monopolistic and anti-competitive, and they will likely get sued over it. At the very least it certainly won’t encourage any technology companies to make services for Unity. What’s the point when Unity will undercut you and still force devs into licensing their own product? Expect far less offerings from third parties in the future.

Ultimately, it screws over indies and smaller devs the most. If you can afford to pay for higher tiers, you don’t pay as much of this nickle and dime fee, but indies can’t afford to on the front end, or often it doesn’t make sense in terms of the volume of games you’ll sell, but then you wind up paying more in the long term. It’ll squash innovation and art-oriented games that aren’t designed around profit, especially. It’s a rotten deal that only makes sense if you’re looking at numbers, and assume everyone will keep using your product. Well, I don’t think people will keep using their product unless they’re stuck. I know one such developer who is stuck, who’s estimating this new scheme will cost them $100,000/month on a free to play game, where their revenue isn’t guaranteed.

Unity is desperately digging its own grave in a search for gold. This is all incredibly short-sighted and adds onto a string of rash decisions and poorly thought through schemes from Unity across the last few years. It’s no wonder Riccitiello sold 2,000 shares last week.

(Okay his selling shares was very likely part of a normal planned sell-through package since executives can’t dump stock all at once, but I wanted to be mean and the timing is funny, so there it is.)


前不久我的Windows开发机炸了,手上只有一台公司配发的MacBook M1 Pro







2.UnityEditor发布项目到XCode工程,在XCode Debug工程时FrameCapture




Xcode frame debugger Unity integration



2.Scheme 打开Info标签






选择创建一个 MacOS Command Line Tool





如果你是M1 芯片的Mac 那么一定不要下载SILICON的版本,一定要下载Intel的版本

M1 配合SILICON版本的编辑器虽然可以提升性能,省电。 但是无法触发截帧!!!!!


GPU FrameCapture的选项不要选择Automatically,直接选Metal


国内用户启动编辑器必须通过UnityHub启动,文档说Executable设置为 UnityHub.app


实际测试 UnityHub.app 加上 启动参数 projectPath 是无法拉起项目的。而应该直接设置为UnityEditor.app









2.找到这个DrawCall用到的Fragment Function双击打开

在Shader中加入一行测试代码 让输出颜色仅仅输出r通道







视频为使用Unity 2021.3 使用 NativeRendering Plugin 在URP 管线下在 RenderFeature中 使用 Apple Variable Rasterization Rates(可变光栅化率)技术进行渲染

“在复杂的3D应用程序中,每个像素都需要执行大量的计算来输出图像,以生成高质量的结果。 然而一旦你的渲染画面随着屏幕分辨率变大,那么渲染更多高品质像素的代价也越大”
为什么要强调 直接 ?因为传统的运动模糊做法需要先对屏幕进行一次截取(Blit),然后通过Stencil,Depth等各种不同的遮罩方案区分出玩家赛车和背景,然后将截取出的画面进行模糊后和原始画面进行合并。这会触发一次RenderPass切换,并伴随着全屏幕的Load/Store,这在移动端上会大量的带宽占用,结局就是设备发热。 而可变速率渲染/光栅化 在drawcall绘制到某个tile时就直接降分辨率了,因此不需要额外的后处理。

在PC上 DX12 PC/移动端上 Vulkan上也都有各自的实现,甚至是加强版,他们把这个技术称为 变分辨率渲染 VRS
他们的技术思想是对于原本点对点的像素采样,现在可以将周围像素组合起来仅仅采样1次,目前支持的组合方式有 1×1,2×2,4×4,2×1,1×2,4×2,2×4,4×4
Vulkan 支持 PerImage, PerPrimitive, PerDrawll级别的可变分辨率渲染
顾名思义就是支持整张画面将分辨率(后处理),PerPrimitive(三角形), PerDrawllCall(单一绘制)
而PerPrimitive方案特别适合大世界植被渲染,对于大范围的植被,我们不但可以做LOD级别的切换,还可以在同一个Instancing DrawCall中根据距离切换渲染分辨率,这样可以让玩家不那么明显的感知到LOD的突兀的三段式模型
对于这项技术的另一个使用,就是全面推翻了之前在手游中普遍使用的 “离屏软粒子”技术, 这项技术我最早实在Gpu Gems3 中看到的
它通过将代价很大的半透明渲染,比如特效,粒子渲染到一个分辨率为原生分辨率 1/4,8/1的 RenderTarget上,用以减少Pixel计算数量,最后回贴到Surface/FrameBuffer上的一种技术。 但是这个技术的代价是需要一张相同降分辨率的Depth/Stencil Texture,切换RenderPass 等涉及到bandwidth的操作,前面提到的后处理模糊处理类似的开销,这些在使用 VRR/VRS技术后,都不需要使用了。
目前主流的高通/联发科芯片产商也直接在驱动中集成了这项技术,相信之后这个技术普及后能见到更多高质量画质,但是省电不发热的游戏 😀
目前高通支持这个技术的GPU为 Adreno 660及之后,MTK天玑系列最新芯片也支持。 但是因为我的安卓手机还没有如此现金,所以使用Iphone 来制作这个Demo, Iphone从 IOS13 起就支持 PerImage/PerRegion级别的 VRR了。
熟悉Metal Shader Language
熟悉Unity URP/RenderFeature开发
熟悉Unity NativePlugin流程
1.Unity中创建RenderFeature,使用该Feature注册任意一个事件用以开启RatemapRenderPass, 比如以 AfterOpaqueEvent
2.RenderFeature中使用CommandBuffer 调用NativePlugin创建一个原生RenderPass,并将Unity的ColorTarget,DepthTarget提供过去
4.RenderFeature中使用CommandBuffer 调用NativePlugin结束原生RenderPass并提交GPU
5.RenderFeature中使用CommandBuffer 调用NativePlugin创建一个BlitPass对降采样过的ColorTarget进行UpScale,并将结果返回给Unity
可以参考官方NativeRendering demo
2.实现CommandBuffer支持的Native Callback函数
有 EventWithData(携带数据) 和 Event(仅发送事件ID int形) 两种
#if (UNITY_IOS && !UNITY_EDITOR) [DllImport (“__Internal”)] #endif private static extern IntPtr GetRenderEventFunc(); #if (UNITY_IOS && !UNITY_EDITOR) [DllImport (“__Internal”)] #endif private static extern IntPtr GetRenderEventAndDataFunc(); CommandBuffer.IssuePluginEventAndData(GetRenderEventAndDataFunc(), (int)EventID, void* data); CommandBuffer.IssuePluginEvent(GetRenderEvent(), (int)EventID);
不能在CommandBuffer中调用如下声明的Native Function,那是在代码中直接调用,无法被CommandBuffer识别的
Unity的资源比如 IndexBuffer,VertexBuffer,RenderTexture 实际上是上层抽象的资源,并非实际资源,但是Unity给了我们一个获取RHI资源的方式
RenderTexture.colorBuffer.GetNativeRenderBufferPtr() RenderTexture.depthBuffer.GetNativeRenderBufferPtr() Texture2D.GetNativeTexturePtr() Mesh.GetNativeIndexBufferPtr(); Mesh.GetNativeVertexBufferPtr(int SubMeshIndex);
id<MTLBuffer> vertexBuffer = (__bridge id<MTLBuffer>)(vertexHandle); id<MTLBuffer> indexBuffer = (__bridge id<MTLBuffer>)(indexHandle); id<MTLTexture> srvTexture = (__bridge id<MTLTexture>)texturehandle; id<MTLBuffer> uvBuffer = (__bridge id<MTLBuffer>)uvHandle;
对了,在传递数据前,最后加上 GCHandle.Alloc 来保证这个数据所占用的内存不会被Unity这边回收
1.intPtr textureHandle = GCHandle.Alloc(RenderTexture.colorBuffer.GetNativeRenderBufferPtr(),AddrOfPinnedObject()); 2.id<MTLTexture> srvTexture = (__bridge id<MTLTexture>)texturehandle;
因为IssuePluginEventAndData 每次只能传递一个 void* data
那么如果你有很多数据要一起传过去就需要调用很多次这个API, 实际上有可以将数据打包成一个Struct进行一起传递
[StructLayout(LayoutKind.Sequential)] struct VRRPassData { public IntPtr colorTex; public IntPtr outputTex; public int validatedata; } GCHandle.Alloc(new VRRPassData(),AddrOfPinnedObject());
记住,一定要记得加上标签LayoutKind.Sequential 表示这是一段连续内存
typedef struct { void* srcTexture; void* dstTexture; int validatedata; }BlitPassData; static void UNITY_INTERFACE_API OnRenderEventAndData(int eventID, void* data) g_BlitData = (BlitPassData*)data;
好了,NativeRendering 大致需要注意的知识点就这些
现在说说Metal VRR用到的API
1.MTLRenderPassDescriptor 用来描述一个Pass,包括ColorTarget,DepthTarget,以及是否使用VRR(通过赋值rasterizationRateMap属性的方式)
2.MTLRasterizationRateMapDescriptor 用来描述一个rasterizationRateMap
2.1 MTLRasterizationRateLayerDescriptor 用来描述Ratemap中层信息,以及划分区域,各个区域的渲染分辨率倍数
3.MTLRenderCommandEncoder Metal通过累计一帧中所有的渲染指令到Encoder后,最后调用EndEncoding进行指令编码为GPU可以理解的语言
4.MTLCommandBuffer 用来生成 MTLRenderCommandEncoder , 在一帧结束后,调用 [MTLCommandBuffer commit] 进行提交到GPU
MTLRasterizationRateMapDescriptor *descriptor = [[MTLRasterizationRateMapDescriptor alloc] init]; descriptor.label = @”My rate map”; descriptor.screenSize = destinationMetalLayer.drawableSize; MTLSize zoneCounts = MTLSizeMake(8, 4, 1);MTLRasterizationRateLayerDescriptor *layerDescriptor = [[MTLRasterizationRateLayerDescriptor alloc] initWithSampleCount:zoneCounts];
8,4,1 表示将屏幕分为横向8块,纵向4块, 1是占位值,Ratemap仅需要两个值,但是因为参数MTLSize构造函数需要3个值,所以第3个值填1即可,实际上用不到第3个值。
for (int row = 0; row < zoneCounts.height; row++) { layerDescriptor.verticalSampleStorage[row] = 1.0; } for (int column = 0; column < zoneCounts.width; column++) { layerDescriptor.horizontalSampleStorage[column] = 1.0; }
1.0表示使用 原生分辨率*1.0倍
layerDescriptor.horizontalSampleStorage[0] = 0.5; layerDescriptor.horizontalSampleStorage[7] = 0.5; layerDescriptor.verticalSampleStorage[0] = 0.5; layerDescriptor.verticalSampleStorage[3] = 0.5
[descriptor setLayer:layerDescriptor atIndex:0]; id<MTLRasterizationRateMap> rateMap = [_device newRasterizationRateMapWithDescriptor: descriptor];
firstPassDescriptor.rasterizationRateMap = _rateMap;
将rateMap赋值给 RenderPassDescriptor
id<MTLRenderCommandEncoder> commandEncoder = [MTLCommandBuffer renderCommandEncoderWithDescriptor:firstPassDescriptor];
通过RenderPassDescriptor 创建出这一帧用来接受渲染指令的RenderCommandEncoder
之后把让Unity的 RenderFeature进行正常渲染
渲染结束后,进行一个BlitPass 上采样输出一张ColorTexture贴合设备分辨率
MTLSizeAndAlign rateMapParamSize = _rateMap.parameterBufferSizeAndAlign; _rateMapData = [_device newBufferWithLength: rateMapParamSize.size options:MTLResourceStorageModeShared]; [_rateMap copyParameterDataToBuffer:_rateMapData offset:0];
1.BlitPass 的Shader需要知道当前屏幕哪些部分被设置了什么样的倍数数据,因此我们要创建一个buffer来存储他们
[renderEncoder setFragmentBuffer:_rateMapData offset:0 atIndex:0];
我们将获取到数据塞入 Metal FragmentShader Buffer中,索引为0
typedef struct { float4 position [[position]]; } PassThroughVertexOutput; fragment float4 transformMappedToScreenFragments( PassThroughVertexOutput in [[stage_in]], constant rasterization_rate_map_data &data [[buffer(0)]], texture2d<half> intermediateColorMap [[ texture(0) ]]) { constexpr sampler s(coord::pixel, address::clamp_to_edge, filter::linear); rasterization_rate_map_decoder map(data); float2 physCoords = map.map_screen_to_physical_coordinates(in.position.xy); return float4(intermediateColorMap.sample(s, physCoords)); }
准备一个fragment shader, 其中 constant rasterization_rate_map_data &data [[buffer(0)]] 索引位置必须和[renderEncoder setFragmentBuffer:_rateMapData offset:0 atIndex:0]; 索引一致
constexpr sampler s(coord::pixel, address::clamp_to_edge, filter::linear); 中使用 coord::pixel 而不是 coord::normalized 表示我们需要使用真实纹理尺寸进行采样,而不是[0-1]的归一化uv坐标
之后在NativePlugin中进行类似Unity的操作 Blit(sourceTex, targetTex, Material)的操作即可
这里涉及到 MTLRenderPipelineDescriptor 的相关操作。 之后回到Unity就能得到正确的结果。
最终补充一个技术点,通过GPUFrameCapture可以发现,图中的白色渲染部分实际上仅占用原始RenderTarget对象(黑色区域) 很小的一个部分,我们可以通过 [Ratemap physicalSizeForLayer:AtIndex] 来获取到白色部分的真实大小,这样我们在创建这张RenderTarget时可以直接使用真实的缩放后的尺寸创建,减少多余内存的浪费。
由于本文不是MetalAPI教程,且假定你是熟悉对应API的同学,因此这里不展开介绍。 本文只是为Unity扩展可变速率渲染功能可行性的一种探索,及技术普及

好久没有更新博客,距离上次分享技术相关有接近2年了。这两年积累好多很有意识的东西很想分享出来,但是因为加入了鹅厂的原因,然后又是在做3A游戏相关,技术上比较难以展开来分享,当然更重要的原因是鹅厂那个量子态的高压线啦,笑死。 不过最近会重回博客,做一些其他方面的技术分享,不过在这之前会先变成类似日志的更新。



最近项目中的小伙伴,打算在大世界地图上刷草,原本的计划是使用Terrain 的笔刷直接绘制




目前网路上大部分的屏幕空间反射都是基于 延迟渲染管线,或者后处理流程来实现的。 其主要原因一个是它是基于屏幕空间的效果,同时对于反射方向还需要考虑反射物体的法线,射线触发方向。 最后的效果部分加上模糊处理等等 对于延迟管线和后处理流程也都顺手拈来。


本篇将基于 Unity 2019 URP Forawrd管线下快速实现一个屏幕空间反射

首先是快速场景搭建,放置1个Plane用作地面,2个Cube 和1个复杂模型(皮卡丘) 这些物件都以Opaque队列渲染

之后在Plane之上摆放另一个等大且位置重合,近Y轴稍微提高(避免Z Fighting)的Plane,但是其渲染队列为Transparent,用于作为反射平面,之所以不复用之前的地面Plane直接做反射是因为我们接下来要利用 URP 中 CameraOpaqueTexture 一张在非透明队列渲染完毕后屏幕截图,以及CameraDepthAttachment 来制作反射, 而一个Transparent队列的Plane不会被上面2张渲染出来,同时可以取到地面Plane的深度

接下来就开始在这个Transprent 的Plane编写反射Shader了


2.以视野方向跟物体相交的位置作为反射起点(反射像素将要填充的位置),以反射向量为方向进行步进, 在每次步进时投影回屏幕空间,进行屏幕空间深度碰撞检测
3.当发现屏幕空间深度和当前步进的检测点相交,或者接近 则视为碰撞成功,返回当前相交点的屏幕空间坐标(UV) 对 CameraOpaqueTexture进行采样像素回贴到反射起点, 否则继续步进,为了防止无限步进导致性能下降,设定一个最远步进距离,超过时,返回天空盒
Continue reading

一般来说使用GpuSkinning 已经能得到很不错的性能了,那么能不能再快一点呢?

测试设备硬件 win10, Intel i7-7700, GPU GTX-1060 6G

可以看到Entity的帧数在 110帧以上, 而传统GPUSkinning 的帧数在 29帧

这个Demo使用的GPU蒙皮方案为 将骨骼矩阵数据以双四元数的方式存储在纹理上,具体实现方法不是这个Demo的重点,大家也可以参考这篇文章

GPU Skinning 加速骨骼动画

首先实现Shader Include



inline float2 BoneIndexToTexUV(float index, float4 param) {
int row = (int)(index / param.y);
int col = index % param.x;
return float2(col * param.w, row * param.w);

inline float3 QuatMulPos(float4 rotation, float3 rhs)
float3 qVec = half3(rotation.xyz);
float3 c1 = cross(qVec, rhs);
float3 c2 = cross(qVec, c1);

return rhs + 2 * (c1 * rotation.w + c2);

inline float3 QuatMulPos(float4 real, float4 dual, float4 rhs) {
return dual.xyz * rhs.w + QuatMulPos(real, rhs.xyz);

inline float4 DQTexSkinning(float4 vertex, float4 texcoord, float4 startData, Texture2D animTex, SamplerState animTexSample) {

int index1 = startData.z + texcoord.x;
float4 boneDataReal1 = SAMPLE_TEXTURE2D_LOD(animTex, animTexSample, BoneIndexToTexUV(index1, startData), 0);
float4 boneDataDual1 = SAMPLE_TEXTURE2D_LOD(animTex, animTexSample, BoneIndexToTexUV(index1 + 1, startData), 0);
float4 real1 = boneDataReal1.rgba;
float4 dual1 = boneDataDual1.rgba;

int index2 = startData.z + texcoord.z;
float4 boneDataReal2 = SAMPLE_TEXTURE2D_LOD(animTex, animTexSample, BoneIndexToTexUV(index2, startData), 0);
float4 boneDataDual2 = SAMPLE_TEXTURE2D_LOD(animTex, animTexSample, BoneIndexToTexUV(index2 + 1, startData), 0);
float4 real2 = boneDataReal2.rgba;
float4 dual2 = boneDataDual2.rgba;

float3 position = (dual1.xyz * vertex.w) + QuatMulPos(real1, vertex.xyz);
float4 t0 = float4(position, vertex.w);

position = (dual2.xyz * vertex.w) + QuatMulPos(real2, vertex.xyz);
float4 t1 = float4(position, vertex.w);

return t0 * texcoord.y + t1 * texcoord.w;

inline void SkinningTex_float(float4 positionOS, float4 texcoord, float4 frameData, Texture2D animTex, SamplerState animTexSample, out float4 output) {
output = float4(DQTexSkinning(positionOS, texcoord, frameData, animTex, animTexSample).xyz,1);


Continue reading

业余时间自己使用JobSystem 优化了一遍DynamicBone,和大家分享下思路,以此互相交流下是否有更好的优化方案。

这一次的代码不会放出工程源码,因为DynamicBone是需要商店付费的,请大家多多支持原作者。 但是本文会放出Job实现的源码


1.UpdateDynamicBones(float t)函数中大量反复计算变量

比如反复计算重力归一化 Vector3 fdir = m_Gravity.normalized;


反复依赖 Transform, 及 localToWorldMatrix 矩阵变换,浪费性能的矩阵操作 m0.SetColumn(3, p0.m_Position), TransformDirection 等


首先JobSystem 的NativeContainer容器是仅支持struct的,因此第一步开刀的是Particle


class Particle
public Transform m_Transform = null;
public int m_ParentIndex = -1;
public float m_Damping = 0;
public float m_Elasticity = 0;
public float m_Stiffness = 0;
public float m_Inert = 0;
public float m_Friction = 0;
public float m_Radius = 0;
public float m_BoneLength = 0;
public bool m_isCollide = false;

public Vector3 m_Position = Vector3.zero;
public Vector3 m_PrevPosition = Vector3.zero;
public Vector3 m_EndOffset = Vector3.zero;
public Vector3 m_InitLocalPosition = Vector3.zero;
public Quaternion m_InitLocalRotation = Quaternion.identity;

修改后,主要是排除Transform的引用, 并使用Unity优化过的数据格式 Unity.Mathematics; 比如float3,float4x4

public struct Particle
public int index;
public int m_ParentIndex;
public float m_Damping;
public float m_Elasticity;
public float m_Stiffness;
public float m_Inert;
public float m_Friction;
public float m_Radius;
public float m_BoneLength;
public int m_isCollide;

public float3 m_EndOffset;
public float3 m_InitLocalPosition;
public quaternion m_InitLocalRotation;

有的同学此时会问了,排除了Transform组件后,算法中大量的 向量从本地空间转换到世界,或者世界空间转换到本地的计算如何进行?

实际上我们只需要支持 RootBone 节点的世界坐标,再配合Particle自身的localPositon + localRotation 是可以一层一层计算出每个Particle的世界坐标的。


//for calc worldPos
public float3 localPosition;
public quaternion localRotation;

public float3 tmpWorldPosition;
public float3 tmpPrevWorldPosition;

public float3 parentScale;
public int isRootParticle;

//for output
public float3 worldPosition;
public quaternion worldRotation;

除了Particle 信息外我们还需要知道根骨骼的世界坐标,以及相关全局变量

比如m_ObjectMove,Gravity 等,可以抽象出一个 Struct Head 来存储这些信息

public struct HeadInfo
int m_HeadIndex;

public float m_UpdateRate;
public Vector3 m_PerFrameForce;

public Vector3 m_ObjectMove;
public float m_Weight;
public int m_particleCount;
public int m_jobDataOffset;

public float3 m_RootParentBoneWorldPos;
public quaternion m_RootParentBoneWorldRot;

准备完基础数据,就要开始JobSystem 化了




1)不展开的话,JobSystem无法将Transform分配给多个Worker 来执行,我猜测是由于在层级的中任意一个Transform发生变化,其子节点的Transform也会发生变化,因此符合Woker中并行优化的逻辑(就是只算自己的)






RootPosApplyJob:IJobParallelForTransform 用来一次性输入根骨骼世界坐标
2.PrepareParticleJob:IJob 用来一次性计算所有Particle此时的世界坐标



5.ApplyParticleToTransform:IJobParallelFor 结算,将所有结果应用到Transform


Continue reading