MultiplyPoint(Vector3 point)
-
这是一个完整的 4×4 变换,包括旋转、缩放、平移,乃至投影(projective transformation)。
-
运算步骤:
-
执行完整矩阵乘法;
-
根据齐次坐标
w做透视除法(w = ...),以支持投影变换等高级功能。
-
-
适用于需要完整变换或投影的场景,但性能略慢。
MultiplyPoint3x4(Vector3 point)
-
专为常规 3D 变换优化,不支持投影。
-
简化流程:
-
使用旋转/缩放的 3×3 矩阵部分;
-
加上平移 (m03, m13, m23);
-
无需计算透视除法。
-
-
更高效,适用于常见位置变换用途。
https://docs.unity3d.com/355/Documentation/ScriptReference/Matrix4x4.MultiplyVector.html
MultiplyVector (v : Vector3) : Vector3
Description
Transforms a direction by this matrix.
This function is similar to MultiplyPoint; but it transforms directions and not positions. When transforming a direction, only the rotation part of the matrix is taken into account.
“齐次坐标”(Homogeneous Coordinates)的核心目的是让平移(translation)这类仿射变换通过矩阵乘法实现,从而统一处理旋转、缩放、平移甚至投影等操作。
传统的欧几里得坐标系中,旋转和缩放属于线性变换,可以通过标准矩阵乘法表示;但 平移不是线性变换,无法直接用 n×n 的矩阵乘法实现。为了统一变换方式,引入了齐次坐标:在 3D 中,将 (x, y, z) 扩展为 (x, y, z, w),构造 4×4 的矩阵来同时支持旋转、缩放和平移。
https://www.cs.usfca.edu/~cruse/math202s11/homocoords.pdf
视图(View)到屏幕空间之间的“透视除法”(Perspective Divide)过程
在图形管线中,一个顶点经过 MVP(模型 × 视图 × 投影)矩阵变换后会进入 裁剪空间(Clip Space),此时其齐次坐标为 (x_c, y_c, z_c, w_c)。然后,GPU 会执行如下操作:
x_ndc = x_c / w_c
y_ndc = y_c / w_c
z_ndc = z_c / w_c
(动的是物体,而不是自己的视野覆盖程度!是物体齐次位移之后,自己的视野,在这之后才出现近大远小)
从 NDC 到屏幕空间
完成透视除法后,还需要进行 视口变换(Viewport Transform),把 NDC 坐标的 [-1, 1] 区间映射到具体的像素坐标,这一步将最终坐标输出到屏幕上
appdata.vertex 的坐标系是哪个?
Unity 的 Shader 中,当你使用 appdata(如 appdata_full)作为顶点着色器(vert)的输入
v.vertex(顶点位置)、v.normal(法线)、v.texcoord(UV 坐标)—都来自于所挂载材质所对应的 网格数据(Mesh)
并且这些数据默认是 以模型的本地坐标系(Object Space) 表示
-
v.vertex是模型空间中的顶点位置; -
v.texcoord是 UV 坐标,同样是网格提供的原始数据,不依赖材质所挂载位置或其他转变。
| 输入字段 | 数据来源 | 坐标系 / 表示方式 |
|---|---|---|
v.vertex | Mesh 顶点 | 模型本地空间 (Object Space) |
v.normal | Mesh 法线 | 模型本地空间 (Object Space) |
v.texcoord | Mesh UV 坐标 | UV 空间,通常局部定义 (UV Space) |
okay … i manage to find the answer in
http://en.wikibooks.org/wiki/Cg_Programming/Unity/Debugging_of_Shaders
Where Does the Vertex Data Come from?
— Within Unity, the answer is that the Mesh Renderer component of a game object sends all the data of the mesh of the game object to OpenGL in each frame
So my initial assumption is correct. The input vertex’s position value is in object-space.

这里直接拖Mesh进去,结果就是如此,如果移动object space,Mesh就是会跟着移动的,所以vert里面接受的位置就是object space里的
model space就是mesh 在场景中挂了组件的那个物体的坐标系

在顶点着色器 (vert) 中把顶点从模型空间(Model/Object Space)转换到父物体的坐标系(Parent Space),这是可以实现的,但需要明确一点:Unity 默认并不会把父物体的变换(Position/Rotation/Scale)直接传递给 Shader;
Shader 接收的是当前带上mesh组件的物体自身的本地矩阵。
父物体空间(Parent Space):Unity 没有内建的将顶点直接变换到父物体局部空间的矩阵。想要做到父空间变换,你需要从脚本中把父物体的局部到世界矩阵的逆矩阵传到 Shader。
实现方式示例
在 C# 脚本中计算父物体处理矩阵:
Matrix4x4 parentWorldToLocal = parent.transform.worldToLocalMatrix;
material.SetMatrix("_ParentWorldToLocal", parentWorldToLocal);在 Shader 的 vert 函数中使用
float4 posModel = v.vertex; // 模型空间
float4 posWorld = mul(unity_ObjectToWorld, posModel);
float4 posParent = mul(_ParentWorldToLocal, posWorld);
v.vertex = posParent; // 转换到父物体局部空间
xxx
这样看来这种配合简直好像就是天生适配一样,在shader里面可以判断cs是否传来命令
然后Graphics.DrawMeshInstancedIndirect会统治这个shader,让这个shader里又额外接受数据,又可以重复处理多次,却不用再场景里创建那么多次的mesh物体
surface shader
void surf(Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
fixed4 m = tex2D(_MetallicGlossMap, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
o.Metallic = m.r;
o.Smoothness = _Glossiness * m.a;
}
surf 函数中所写的仅是部分可操控内容。你可以设置:
-
Albedo(漫反射颜色) -
Alpha(透明度) -
Normal(法线) -
Metallic(金属度) -
Smoothness(光滑度) -
Occlusion(遮蔽,也称 AO) -
Emission(自发光)
具体表现方式
-
非金属(Metallic=0):材质会有明显的漫反射,Albedo 直接决定漫反射颜色;镜面反射则取决于 Fresnel 和统一的 F₀ 值。
-
金属(Metallic=1):不再表现漫反射,Albedo 会被用作镜面反射颜色,营造金属效果。
为什么在 PBR(Physically Based Rendering,物理渲染)中要提出“金属度”(metalness)这一路径?既然这是一个抽象概念,为什么不用一个更简洁的方式来表示材质的全部特性?
简化数据与优化资源使用
-
使用金属度这种黑白掩码方式,可以让材质“是否为金属”这一判断变得非常高效,同时易于打包与存储在灰度纹理里,从而节省内存和纹理通道。Blender Stack Exchange+10chaos.com+10chaos.com+10
-
例如在 Substance 或 Unity 中,金属度和光滑度常被合并存储在同一张贴图的不同通道中,提升性能和贴图利用率。Mediumdanthree.studio
符合物理现实,更易理解
-
在真实世界中,材料要么是“导电金属”(metal),要么是“非金属”(dielectric),而没有中间状态。金属度的概念将这种二分状态引入 PBR,让 Shader 根据这一属性选择正确的光照模型。Medium+9chaos.com+9chaos.com+9
-
虽然在现实中没有“半金属”的物理存在,但在艺术表达和风格需求下,0–1 区间可以帮助制作出“部分锈蚀”或“涂层金属”的效果。Reddit+1danthree.studio
提升正确性并避免错误使用
-
金属度的工作流程帮助确保材质逻辑正确:非金属应保留漫反射(Diffuse),金属则以镜面反射为主。这种协议帮助艺术家避免创建不符合物理规律的材质,增强一致性。discussions.unity.com+6polycount+6Blender Stack Exchange+6
其实,“金属度”并不是随意选的一个词,而是反映了真实物理的属性:在现实中材料分为“金属”与“非金属”,金属度正是这个区分。相比只使用“高光强度”或“亮度”,金属度提供了更明确、物理正确的渲染路线。其他表达方式可能更含糊,不利于构建统一、可控的 PBR 工作流程。已广泛用于 Unity、Unreal、Substance 等工具中
https://blender.stackexchange.com/questions/229787/metallic-vs-specular
here's plenty of room for confusion here. Metallic materials have specular, so in that sense, it's inappropriate to draw a distinction between specular and metallic-- the more proper distinctions would be between specular and diffuse and between metallic and dielectric.
However, metallic and specular are used to describe two different types of shaders that are sometimes used in conjunction. They are often referred to as the metallic workflow vs. the specular workflow.
A metallic workflow does not mean that it only represents metals. Anything with a "metalness" input, like the Principled BSDF, represents a metallic workflow, and 0.0 is a perfectly valid input for metalness.
金属度工作流程做出了一些通常有效的假设,从物理角度来说。它假设金属没有漫反射成分——它们有黑色漫反射。它假设金属可以有彩色高光,而非金属则不能。(这里可能会有一些变化,就像在principled的高光色调中一样,但请注意,您不能从基色改变高光的色调,只能改变饱和度。)在Blender的Principled BSDF的情况下,它还内置了特定的菲涅耳响应。
contrast, a specular workflow is based less on physical features, and more on how 3D computer graphics work-- and, maybe, more on how artists think.
It is less likely to be energy conserving, and is more likely to just compute a diffuse, compute a specular, and add them together, meaning that the albedo of a specular workflow material can be hard to calculate
- it's not just a function of the diffuse and specular colors, but of the roughness and Fresnel as well.
一些Blender艺术家是极端的PBR狂热者,但许多其他人调整材料以渲染他们脑海中看到的画面,这并不总是基于物理的。
实物体不是由表面构成的,而是由体积构成的,就像真实物体不是由宏观的平面三角形构成的
没有一种完美的金属是金属度工作流程可以模拟的。存在许多真实材料,它们既不是由金属度工作流程也不是由镜面反射工作流程正确表示的。一些稀有材料——例如珍珠母——用镜面反射工作流程比用金属工作流程模拟得更好。
在Blender中,Principled BSDF是一个金属度工作流程,而镜面着色器(仅在Eevee中可用)是一个镜面工作流程。如果您想的话,可以使用两者——您可以在它们之间混合着色器。
根据Blender文档,金属度只是一个表示材质金属程度的数值。通常,该值要么是0(非金属)要么是1(金属)。将光泽度理解为材质高光强度的简单表示。(0 = 无高光,1 = 正常高光。)
实际上,镜面反射是某种材料的特定反射类型。当你向某物照射明亮的光线时,那物体会反射这束光线。你所看到的反射光中心就是镜面反射,而光线周围的则是漫反射。
The Principled BSDF that combines multiple layers into a single easy to use node. It can model a wide variety of materials.原則BSDF,將多層結合成單一易於使用的節點。它可以模擬各種各樣的材料。
Principled BSDF 基于OpenPBR表面着色模型,并提供与在其他软件中找到的类似PBR着色器兼容的参数,例如迪士尼和标准表面模型。从Substance Painter等软件中绘制或烘焙的图像纹理可以直接链接到该着色器中的相应输入
底层是由金属、漫反射、次表面散射和透射组件混合而成。大多数材料将使用这些组件之一,尽管可以在它们之间平滑混合。

max(_TailWidth, 1e-4):避免 _TailWidth 为 0 时除法报错(1e-4 是一个极小的安全值)。
// 围绕指定“上”轴做平面旋转(yaw)
float3 rotateAroundUp(float3 p, float angle, int upAxis)
{
float s, c; sincos(angle, s, c);//给一个角度,创造旋转矩阵
if (upAxis == 0)
{
// X 作为上轴 -> 旋转 yz
float2 yz = mul(float2x2(c, -s, s, c), p.yz);
p.y = yz.x; p.z = yz.y;
}
else if (upAxis == 2)
{
// Z 作为上轴 -> 旋转 xy
float2 xy = mul(float2x2(c, -s, s, c), p.xy);
p.x = xy.x; p.y = xy.y;
}
else
{
// Y 作为上轴 -> 旋转 xz
float2 xz = mul(float2x2(c, -s, s, c), p.xz);
p.x = xz.x; p.z = xz.y;
}
return p;
}
compute shader的cs脚本Start
建立 “间接绘制参数缓冲” _drawArgsBuffer
_drawArgsBuffer = new ComputeBuffer(1, 5*sizeof(uint), ComputeBufferType.IndirectArguments);
_drawArgsBuffer.SetData(new uint[5]{
BoidMesh.GetIndexCount(0), // 0: 每实例的索引数 indexCountPerInstance
(uint)BoidsCount, // 1: 实例数量 instanceCount
0, // 2: startIndexLocation
0, // 3: baseVertexLocation
0 // 4: startInstanceLocation
});
-
类型
IndirectArguments:专供Graphics.DrawMeshInstancedIndirect使用。 -
这 5 个
uint的固定布局来自 Unity/D3D 的 DrawIndexedInstancedIndirect:
[ indexCountPerInstance, instanceCount, startIndex, baseVertex, startInstance ] -
indexCountPerInstance取自BoidMesh的第 0 个子网格;instanceCount就是你的鱼/boid 数量。只要更新这块缓冲(比如实例数变了),下一次 Draw 就会自动按新参数渲染,无需走 CPU。
所以这里可能还可以筛选出instance?
MaterialPropertyBlock:写入一个“无关但唯一”的参数
_props = new MaterialPropertyBlock();
_props.SetFloat("_UniqueID", Random.value);
-
给材质属性块写一个随机
_UniqueID,不参与着色,只是作为“每次/每批次唯一标识”。 -
这么做常用于绕开某些实例化/批处理的合批或缓存问题(老版本或特定平台上可能出现“状态被复用”的怪异现象)。有了唯一值,Unity 会认为这次绘制有独立的属性集,避免错误复用。
分配 CPU 侧 boid 数据数组 + 找到两个 ComputeShader kernel
boidsData = new GPUBoid_Multilateration[BoidsCount];
kernelMoveHandle = _ComputeFlock.FindKernel("Move");
kernelMultilaterationHandle = _ComputeFlock.FindKernel("ComputeMultilateration");
-
boidsData是 CPU 端的初始数据数组(位置、速度、方向、噪声种子等)。 -
FindKernel拿到计算着色器里对应函数的句柄,后面Dispatch要用。
逐个 boid 写入初始值 & 噪声偏移
for (int i = 0; i < BoidsCount; i++){
boidsData[i] = CreateBoidData(); // 位置/速度等初值
boidsData[i].noise_offset = Random.value*1000f; // 每只 boid 的噪声偏移,避免同频
}
noise_offset 让每只 boid 的噪声相位不同,不然会同步摆头,像“合唱团”。
把 boid 数据放进 GPU 缓冲(结构化缓冲)
BoidBuffer = new ComputeBuffer(BoidsCount, 48);
BoidBuffer.SetData(boidsData);
-
这是 GPU 端存放所有 boid 状态的
ComputeBuffer(通常 HLSL 里StructuredBuffer/RWStructuredBuffer)。 -
48是步幅(stride,字节数)——必须与 HLSL 里struct的实际内存布局完全一致。
例如--:float3(12) + float3(12) + float4(16) + float(4) + 对齐填充(4) = 48,这取决于你GPUBoid_Multilateration的字段和对齐。步幅不对会导致 GPU 读到错位数据(位置飞天/NaN),是常见坑。
而这里,,其实是有些模糊空间的,也只是记住这里会有些空间调整,可以优化性能,也可能产生bug,,这里只有36字节

设置成36和48没什么区别,也许unity自动处理了

,但要是设置成128,,问题就很大了,既没有显示,还掉帧了三分之一,64字节也一样的结果
我靠,,,直接崩了unity,,居然还会有这样的影响


如果去掉padding

看 漏了,是12个字节,,还有个float3 direction
这次倒是刚刚好了,还是记住要注意一下吧,太深了估计就容易扯皮了
换之前的Draw试试,对比之后没有什么区别

看来compute shader buffer接受的时候,对里面的值是如果有则有,没有则不管,所以padding这种没用上的属性即使在cs脚本里删掉了,也没有问题
把全局常量塞给 ComputeShader
_ComputeFlock.SetInt("BoidsCount", BoidsCount);
-
让
Move/ComputeMultilateration这两个 kernel 在计算时知道总数量,以便索引/循环/分组处理。 -
之后再update里set各种值,实际上是对值的更新,而不是set之后下一帧就消失
接下来通常还会有(在别处的代码里):
-
把
BoidBuffer绑定给 ComputeShader 的两个 kernel:
SetBuffer(kernelMoveHandle, "boidBuffer", BoidBuffer);
SetBuffer(kernelMultilaterationHandle, "boidBuffer", BoidBuffer); -
把
BoidBuffer绑定给 渲染材质:
BoidMaterial.SetBuffer("boidBuffer", BoidBuffer); -
每帧
Dispatch两个 kernel(更新 boid 状态),然后用这个来画
Graphics.DrawMeshInstancedIndirect(
BoidMesh, 0, BoidMaterial,
new Bounds(Vector3.zero, Vector3.one*1000f),
_drawArgsBuffer, 0, _props
);
小坑提示
-
Bounds要够大,否则被视锥剔除了就什么也不画。 -
BoidMesh必须有 index buffer(GetIndexCount(0)> 0)。 -
Stride 对齐要跟 HLSL 结构严格一致。
-
如果换 URP/HDRP,确认 Shader/HLSL 路径、宏(
UNITY_PROCEDURAL_INSTANCING_ENABLED)与缓冲命名一致。
“位置飞天/NaN”
通常就是因为 C# 定义的结构体步幅(stride) 和 HLSL shader 中的 struct 内存布局不一致 导致的。这种不一致会让 GPU 读到错位数据,从而出现无限大、NaN、不稳定的位置值。
Unity 文档明确要求:
-
如果使用
ComputeBufferType.Structured或GraphicsBuffer.Target.Structured,那么 stride 必须与 HLSL 中对应StructuredBuffer<T>内的 struct 大小一致,并且:-
必须是 4 的整倍数。
-
推荐使用 16 的整倍以提高兼容性和性能。
Unity Documentation+1
-
如果 C# 端的步幅少写了 padding、或写多了、或结构变量未按 HLSL 对齐规则排列,就会让 GPU 每次读取 struct 时错位,比如:
-
读取了其他内存位置的数据作为 position → 出现巨大的值。
-
或读取根本未初始化的区域 → NaN或漂移。
StackOverflow 上很多类似问题,例如 stride 应为 64B 但写成 24B,就会报错或数据错乱。Reddit
怎么确保 stride 正确
| 方法 | 说明 |
|---|---|
| 手动计算 | 基于结构体字段大小累加,比如 Vector3(xyz)=12B + padding 4B + float=4B ... |
UnsafeUtility.SizeOf<T>() | Unity 提供的工具函数,取得正确的大小,支持自定义 struct (catlikecoding.com, Stack Overflow) |
Marshal.SizeOf() | C# 内建方式(需 [StructLayout(LayoutKind.Sequential)])(Stack Overflow) |
NaN 是 Not a Number 的缩写,意思是“不是一个数值”。
它是 IEEE 754 浮点数标准里专门保留的一种特殊值,用来表示计算结果无效或不可表示的情况,比如:
| 计算 | 结果 | 为什么是 NaN |
|---|---|---|
0.0 / 0.0 | NaN | 除以 0,结果不确定 |
sqrt(-1) | NaN | 平方根不能作用于负数(在实数域中) |
∞ - ∞ | NaN | 无限减无限,结果不确定 |
0 * ∞ | NaN | 逻辑上无法定义这个值 |
在 Unity / Shader 里的影响
-
顶点坐标是 NaN → 模型会瞬间飞到很远的地方(因为 GPU 会把 NaN 当作一个极端值处理),甚至渲染出一条线刷满整个屏幕。
-
颜色是 NaN → 会导致像素渲染出奇怪的颜色,或者后期特效(Bloom、ToneMapping)全屏闪烁。
-
位置计算是 NaN → Boid、粒子、物理对象会突然乱飞或消失。
检测方法
-
在 C# 里:
if (float.IsNaN(value)) { Debug.Log("NaN detected!"); }
-
在 HLSL/Shader 里:
if (any(isnan(myValue))) { /* 处理 */ }
如果 stride 对不齐或数据越界,GPU 读出来的数会直接变成 NaN,结果就是物体飞天或者场景炸掉。
Multilateration is a technique for determining the location of an object by measuring the arrival times of signals at multiple receivers. It's often used in navigation and surveillance systems, especially in air traffic control. The core principle is that by measuring the differences in signal arrival times at different locations, the position of the signal's source can be calculated.
Update()
就是在每帧做三件事:①用 ComputeShader 先算数据(两步:定位/群集移动),②把算好的 ComputeBuffer 绑定到材质,③用 GPU 的“间接实例化”一次性把所有 boid 画出来。
先跑“定位/测距”那一步(multilateration)
_ComputeFlock.SetBuffer(kernelMultilaterationHandle, "boidBuffer", BoidBuffer);
_ComputeFlock.Dispatch(kernelMultilaterationHandle, BoidsCount, 1, 1);
-
把 boid 状态缓冲(
BoidBuffer)绑给 kernel"ComputeMultilateration"(名字说明:多边测量/定位)。 -
Dispatch(BoidsCount,1,1)启动线程组运行这个 kernel。
注意:这里的参数要与你在 ComputeShader 里写的numthreads(x,y,z)对应。常见的做法是按线程组分发(见第 3 步的写法),如果这里用BoidsCount,那你的 kernel 通常应是numthreads(1,1,1);否则就会跑超了。稳妥做法是在 kernel 里加索引越界保护:hlsl
uint i = ...; if (i >= BoidsCount) return;
作用:更新每只 boid 与“基站/锚点”的距离、或由距离反推位置相关的数据(你的工程命名看起来是这个意思)。
设置当帧用到的全局参数
_ComputeFlock.SetFloat("DeltaTime", Time.deltaTime);
_ComputeFlock.SetFloat("RotationSpeed", RotationSpeed);
_ComputeFlock.SetFloat("BoidSpeed", BoidSpeed);
_ComputeFlock.SetFloat("BoidSpeedVariation", BoidSpeedVariation);
_ComputeFlock.SetVector("FlockPosition", Target.transform.position);
_ComputeFlock.SetFloat("NeighbourDistance", NeighbourDistance);
-
把 本帧时间步长、转向速度、基础速度、速度扰动、目标点(朝向/吸引中心)、邻居感知距离 等,传入 ComputeShader,供移动规则计算使用。
再跑“群集移动”那一步(move)
_ComputeFlock.SetBuffer(kernelMoveHandle, "boidBuffer", BoidBuffer);
_ComputeFlock.Dispatch(kernelMoveHandle, BoidsCount / GROUP_SIZE + 1, 1, 1);
-
把同一个
BoidBuffer绑给"Move"kernel(更新速度/位置/方向等)。 -
这里采用按线程组分发:如果 ComputeShader 里写的是
numthreads(GROUP_SIZE,1,1),那 CPU 侧分发组数用BoidsCount / GROUP_SIZE + 1(向上取整)是常见写法。
一样要在 kernel 内防越界:uint i = groupID.x * GROUP_SIZE + groupThreadID.x; if (i >= BoidsCount) return;
作用:根据 flocking 规则(对齐/聚合/分离 + 目标点吸引 + 随机扰动等),更新每只 boid 的状态到 BoidBuffer。
// BoidMaterial.SetMatrix("_LocalToWorld", transform.localToWorldMatrix);
// BoidMaterial.SetMatrix("_WorldToLocal", transform.worldToLocalMatrix);
如果你是把一整个群体作为一个对象来位移/旋转(而不是每只 boid 都有世界坐标),可以把父物体的变换传进 shader 统一乘。
但你的方案已经在 ComputeShader 中把位置算到世界空间,并在材质里直接用,所以可以不需要这两行。
1318

被折叠的 条评论
为什么被折叠?



