Posts

一篇文章说透计算着色器的SV_DispatchThreadID

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com SV_DispatchThreadID 是HLSL(High-Level Shading Language)中的一个语义(Semantic),用于在Compute Shader中获取当前线程的全局线程ID。它是Compute Shader中非常重要的一个概念,用于确定每个线程处理的数据或任务。 SV_DispatchThreadID 的含义 全局线程ID: SV_DispatchThreadID 是一个 uint3 类型的变量,表示当前线程在全局线程空间中的ID。 它的三个分量(x、y、z)分别表示线程在X、Y、Z三个维度上的全局索引。 线程的全局位置: SV_DispatchThreadID 的值范围由程序语言层(例如C#) 的Dispatch 调用,和HLSL语言中的 numthreads 语句定义。 例如,如果调度了 Dispatch(10, 20, 30),并且 numthreads 是 [8, 8, 1],那么: SV_DispatchThreadID.x 的范围是 [0, 10*8-1](即 [0, 79])。 SV_DispatchThreadID.y 的范围是 [0, 20*8-1](即 [0, 159])。 SV_DispatchThreadID.z 的范围是 [0, 30*1-1](即 [0, 29])。 用途: SV_DispatchThreadID 通常用于确定当前线程处理的数据索引。 例如,在处理2D纹理时,SV_DispatchThreadID.xy 可以直接用作纹理坐标。 在处理1D数组时,SV_DispatchThreadID.x 可以直接用作数组索引。 SV_DispatchThreadID 的计算方式 SV_DispatchThreadID 的值是由以下两个因素决定的: 线程组的大小(由 numthreads 定义): 例如,[numthreads(8, 8, 1)] 表示每个线程组有 8x8x1 = 64 个线程。即是说,numthread语句,是声明“线程”的。 线程组的数量(由 Dispatch 调用定义):

层次化Z缓冲区拣选算法学习笔记

1 Nick Darnell的Hiz算法总结和分析笔记 基于层次化Z缓冲区的遮挡拣选(Hierarchical Z-buffer occlusion culling)的实现方式有多种,其中有一种是基于论文Real-Time Rendering (Section 3.3.3)。 1.1 层次化Z缓冲区(Hierarchical Z-buffer,HiZ)剔除步骤 让美术准备好用来遮挡住其他物体的遮挡体(occlusion geometry) CPU端:获取到所有的遮挡体,且将在视截体之外的给剔除掉 GPU端:把上一步剔除剩下的遮挡体,将其深度值渲染到深度缓冲区D-RT0 GPU端:对D-RT0进行采样,并填充整个mipchain CPU端:对当前场景中的所有可见物体,获取到其物体的包围球 GPU端:把上一步获取到的物体包围球,计算该包围球对应的屏幕空间宽度,然后使用该宽度计算 mip 级别,从步骤 4 中生成HiZ 贴图中进行采样 CPU端:读回计算着色器的缓冲区输出 1.2 对分层Z缓冲区进行降采样的HLSL代码 上述步骤中的第4步用到的降采样的方式如下: 取当前的,右边一个、下方一个和右下方一个像素进行比较,取其值最大者,作为降采样像素中的新深度值。下面两个图是降采样前后版本的示例,黑色表示较近的深度,像素越白,深度值越远/越高。 降采样HLSL代码如下所示 float4 vTexels; vTexels.x = LastMip.Load( nCoords ); vTexels.y = LastMip.Load( nCoords, uint2(1,0) ); vTexels.z = LastMip.Load( nCoords, uint2(0,1) ); vTexels.w = LastMip.Load( nCoords, uint2(1,1) ); float fMaxDepth = max( max( vTexels.x, vTexels.y ), max( vTexels.z, vTexels.w ) ); 1.3 层次化Z缓冲区的剔除核心算法 cbuffer CB { matrix View; matrix Projection; matrix ViewProjection; float4 FrustumPlanes[6]; // 视截体的6个面,基于世界坐标,每个面的法线朝外 float2 ViewportSize; // 视口的高和宽,基于像素 float2 PADDING; }; // GPU只读的缓冲区,因为声明了使用t0着色器,故而每一个点是一个float4 // float4的xyz分量记录了包围球的球心坐标,w分量记录了球的半径。基于 // 世界坐标系 StructuredBuffer Buffer0 : register(t0); // Is Visible 1 (Visible) 0 (Culled) // GPU可读写的缓冲区,每一个点是一个uint,值为1时该点可视,0时该点不可视 RWStructuredBuffer BufferOut : register(u0); Texture2D HizMap : register(t1); SamplerState HizMapSampler : register(s0); // 计算给定的点vPoint到平面方程的距离 // vPlane: 四个xyzw分量,分别包含了平面方程的四个系数abcd: ax + by + cz = d // vPoint: Point to be tested against the plane.

使用mipmap streaming来优化GPU的纹理内存

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com 本文所涉及到的API和Unity3D编辑器都是基于6000.0.25f1c1版本 1 mipmap streaming 简介 使用 mipmap 流来限制 GPU 内存中纹理的大小。 1.1 mipmap streaming的工作原理 对于每个被摄像机观察到的纹理,Unity会自动计算并加载其特定级别的mipmap,而不是加载所有级别。这意味着 Unity 只会将每个纹理的指定mipmap level的内容从磁盘传输到 CPU 和 GPU。 Unity 会以尽可能高的分辨率加载 mipmap level,但如果较高分辨率的 mipmap level不符合您设置的内存限制,则使用较低的 mipmap level。mipmap可预配置 Unity 在 GPU 上缓存指定mipmap level的纹理内容,以避免重复加载。 1.2 mipmap streaming的限制 Unity不支持对地形纹理使用mipmap streaming,因为 Unity 始终需要最高分辨率的 mipmap level。 Unity不支持对纹理数组、cubemap,3D纹理使用mipmap streaming。 如果使用Graphics.DrawMeshNow等 API来渲染纹理,Unity 将无法获得计算 mipmap level所需的信息。使用Texture2D.requestedMipmapLevel API 手动设置纹理的 mipmap level,或者禁用mipmap streaming。 如果纹理有tiling和offset属性,但又没指定使用_ST属性,Unity可能无法正确计算这种纹理的mipmap level。 2 启用 mipmap streaming 要启用 mipmap streaming,按照以下步骤操作: 点击【Edit|Project Settings】菜单项,弹出【Project Settings】面板,选中【Quality】项 在【Texture】部分中,启用【Mipmap Streaming】 在默认情况下,这时候为场景中的所有摄像机都打开了mipmap streaming。在编辑器中,mipmap streaming在edit mode或者play mode下都处于开启状态。

Software Virtual Texture 学习笔记

在MegaTexture的基础上,id Software进一步提出了Virtual Texture的概念,这个概念取自于Virtual Memory。与虚拟内存类似的是,一个很大的Texture将不会全部加载到内存中,而是根据实际需求将需要的部分加载;与虚拟内存不同的是,它不会阻塞执行,可以使用更高的Mipmap来暂时显示,它对基于block的压缩贴图有很好的支持。 基本思路是:将纹理的Mipmap chain分割为相同大小的Tile或Page,这里的纹理是虚纹理,然后通过某种映射,映射到一张内存中存在的纹理,这里的纹理是物理纹理,在游戏视野发生变化的时候,一部分物理纹理会被替换出去,一部分物理纹理会被加载。 这样的机制不仅仅减少了带宽消耗和内存(显存)消耗,也带来了其它好处。比如有利于合批,而不用因为使用不同的Texture而打断合批,这样可以根据需求来组织几何使得更利于Culling。当然合批的好处是states change变少,LightMap也可以预计算到一张大的Virtual Texture上用来合批。 1 地址映射 地址映射在Virtual Texture中是一个很重要的环节,即是“如何将一个virtual texture上的texel,对应地映射到phyiscal texture上去”。同时还需要处理“假如高分辨率的page没有加载的话,如何获得已加载的相对应的低分辨率的page” 1.1 四叉树映射 使用四叉树主要是为了和Mipmap对应,也就是每个低MIP的Map会对应有四个高MIP的Map,四叉树中只存储加载的Mipmap信息。这里的对应关系就是每个加载的Virtual Texture的Page对应一个四叉树的节点,具体的计算如下: 这里存在每个四叉树的节点中的内容就是bias和scale,这样就可以将虚拟纹理的地址转换成物理纹理的地址。如果没有找到,也可以用父节点的地址来得到低分辨率。但是这里要找到对应的节点需要搜索这个四叉树,搜索的速度取决于树的高度,也就是Mipmap的层级,在差的低MIP的地址上效率会比较低。 1.2 单像素对应虚纹理的一个Page的映射 为了减少索引,首先容易想到的就是,为每个虚纹理的Page都存储一份信息,这样就能直接转换了。这个方案就是创建一个带Mipmap的Texture,一个Texel对应虚纹理的一个Page,Texel的内容就是四叉树映射里面的bias和scale。 假如对应的MIP没有加载,存储的就是高MIP的转换信息,这样显然就提高了地址转换的效率。但是这会带来内存增加,因为我们需要每个虚纹理的Page都对应一个Texel。其中bias和scale都是二维的向量,即使设计虚纹理和物理纹理的比例一致,我们也需要至少scale、SBias、TBias三个量,而且这三个量的精度要求很高,至少需要16bit的浮点数精度。如果要达到这样的精度就需要F32*4的纹理格式,那么必然会产生一个巨大的映射纹理,因此需要减小映射纹理的大小。 1.3 双纹理映射 这个方案仍然有一个对应每个虚纹理Page的Texture,但是不同的是,纹理的内容存储的是物理纹理Page的坐标,用这个坐标再去索引另外一张Texture。另外一张贴图的内容才是bias和scale,但不是每个虚拟纹理,而是每个物理纹理Page一个Texel。下图是虚拟纹理对应的Texture: 这样就减少了映射纹理的大小,但是同时多了一次纹理查询。 2.1.4 Page和MIP level映射 总结上面两个基于映射纹理的方案,要么是纹理需要很大的存储,要么是需要多次查询。如果从映射纹理比较大的角度考虑优化,可以考虑适当减少每个像素的大小,这个方案就是从这个角度出发的。在这个方案中,仍然是每个虚拟纹理的Page对应一个texel,但是存储的内容是物理纹理Page的Offset和虚拟纹理所在的MIP level。 这样存储的好处就是,Page Offset对精度的要求没有那么大,用32bit的Texture即可。当然也可以压缩到更小格式的纹理中,如RGB565。这种方案的使用最广泛,基本各家引擎的实现都使用了这种方案。 2.1.5 HashTable映射 这是最直接的方法,好处是节省内存,查询速度快,但是当遇到没有加载的virtual Page的时候,需要多次查询。这个和四叉树还有一个问题,即如何设计一个GPU友好的数据结构。 2.2 Texture Filtering 由于虚拟纹理并没有完整加载,所以各种采样过滤在Page的边界会有问题,我们需要自己设计解决这些问题的方法,适当的使用软实现的采样。 2.2.1 Bi-linear Filtering 这个解决方案比较简单,就是给Physical Page加上一个像素的border。 2.2.2 Anisotropic Filtering Anisotropic Filtering可能需要更多的相邻像素,假如我们需要支持8倍的Anisotropic Filtering,那我们需要采样步长为4的相邻像素,也就是我们的border要增加到4个像素。增加4个像素的border会增加Physical Texture的大小,但是带来了一个好处,就是适配了block compression。 具体实现可以分为软实现和硬实现,硬实现放到下文的Tri-linear说,这里说软实现。软实现其实就是在Shader中实现Anisotropic Filtering的算法,在决定采样的MIP level的时候,需要把虚纹理相对于物理纹理的比例考虑进去,剩下的就是正常的Anisotropic Filtering。 2.2.3 Tri-linear Filtering Tri-linear Filtering的实现方案可以分为两种:一种是软实现,一种是硬实现。 所谓的软实现与Anisotropic Filtering一样,在Shader中实现Tri-linear Filtering。也就是说,需要在Shader中计算MIP level,然后进行两次地址的转换,采样两个物理纹理的Page后进行插值。 硬实现的方法是直接给物理纹理生成一个一层的Mipmap,然后利用硬件去直接采样。同样的,对于Anisotropic Filtering,也打开Anisotropic Filtering直接进行采样。这样的好处当然是由于硬件的加速,采样的效率会提升,但是这样同时会导致增加25%的纹理大小,而且由于Mipmap的边界会变成两个像素,对于block compression和超过4倍的Anisotropic Filtering来说,在遇到Page的边界时都会出现问题。

Sparse virtual textures 学习笔记

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com Nathan Gauër的Sparse virtual textures 稀疏虚拟纹理概述 sparse virtual texture(SVT)方案是由Id Software提出,用来解决Texture加载问题的一套解决方案,解决了带宽和内存不足的问题。 在Id Software的方案之后,业界又相继提出以下的的方案: Procedural Virtual Texture Adaptive Procedural Virtual Texture Hardware Virtual Texture 稀疏虚拟纹理的历史沿革 最早期的SVT思想,是受Mesh的LOD启发。不同LOD的Mesh对应着不同的网格面数,对应地也应加载不同大小的texture。 在1998年,Tanner发表了一篇《The Clipmap: A Virtual Mipmap》的论文,文中提到了一种叫clipmap的技术。后来被Id Software引入并改进,称之为MegaTexture clipmap的基本思想是:设置一个mipmap大小的上限,超过这个上限的mipmap会被clip掉,也就是不会加载到内存中 Software Virtual Texture 在MegaTexture的基础上,Id Software提出了virtual texture的概念,这个概念取自于virtual memory。与virtual memory似的是,一个很大的texture将不会全部加载到内存中,而是根据实际需求将需要的部分加载。如下图所示: 基本思路是:将虚拟纹理的Mipmap chain分割为相同大小的Tile或Page,这里的纹理是虚纹理,然后通过某种映射,映射到一张内存中存在的纹理,这里的纹理是物理纹理,在游戏视野发生变化的时候,一部分物理纹理会被替换出去,一部分物理纹理会被加载。 稀疏虚拟纹理(sparse virtual texture)。简单地说,就是在着色器中重新实现分页(pagination),可以拥有无限的纹理,同时保持 GPU 内存使用量恒定。 1 步骤 1 - 手工制作分页 1.1 分页概述 要理解 SVT 如何工作,首先要了解什么是分页: 在大多数计算机上,数据存储在 RAM 中。RAM 是一个线性缓冲区,其第一个字节位于地址 0,最后一个字节位于地址N。因为某些原因,使用真实地址并不方便。因此有人发明了分段(segmentation),后来演变为分页。这个想法很简单:使用虚拟地址(virtual address),由 CPU 将其转换为实际 RAM 地址。 在上图中:左侧是虚拟内存,可以看见虚拟内存的地址是线性连续的。共分为4个page。虚拟内存中每个page,都线性映射到page table中的一个entry。 下面举例说明,利用page和page table,如何将一个virtual RAM address(VRA)映射到physical RAM address(PRA)。例子中的page的大小是4KB,即4096个字节。每个虚拟地址VRA,可以用公式表示为:

什么是UV density

UV Density(UV密度)是指在3D建模和纹理映射过程中,每单位面积上UV坐标的密集程度。它描述了UV坐标如何在模型的表面上分布,直接影响纹理的细节和清晰度。 UV坐标是将三维模型的表面(即网格)展开到二维平面上所使用的坐标系。每个顶点都有一个对应的UV坐标,用于在纹理贴图中定位该顶点上的纹理图像。 UV密度高意味着同样的纹理会在模型的更多区域覆盖,因此每个单位面积上的纹理细节更丰富,通常会表现出更高的纹理分辨率。 UV密度的影响: 纹理清晰度:UV密度越高,纹理的细节越清晰,尤其在大模型或复杂形状的区域。 纹理失真:如果UV密度不均匀,可能会导致某些区域的纹理出现拉伸或压缩,影响视觉效果。 性能:较高的UV密度意味着更多的纹理数据,可能会影响渲染性能,尤其是在低性能的设备上。 如何控制UV密度: 在3D建模时,合理分配UV空间,确保模型表面的大致区域都有足够的纹理空间。 在纹理贴图时,可以通过调整纹理的分辨率或者UV映射方式来优化UV密度。 总之,UV密度是纹理映射的重要指标,合理控制它能确保纹理效果的质量和性能。

Unity3D中摄像机的FOV和Camera.ScreenToWorldPoint方法的关系

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com 在Unity3D编程中经常会碰到以下的问题: 比如一开始的FOV为60度,后来设置为22度,可以通过怎样的公式,使得使用22度FOV对应生成的3D位置点,通过这个公式,可以转换到其位置点和使用60度生成的一样呢 当从一个FOV为60度的视角切换到22度的视角时,使用Camera.ScreenToWorldPoint函数生成的3D位置点会发生变化,因为FOV直接影响视锥体的宽度和高度。要解决这个问题,需要通过调整视锥体的缩放因子,来抵消FOV变化带来的位置偏移。 问题分析 当FOV变化时,屏幕坐标对应的世界坐标会随之缩放。假设有一个屏幕坐标screenPos,在FOV为60度时通过Camera.ScreenToWorldPoint生成一个3D位置点P60。当FOV变为22度时,假设生成的3D位置点为P22。我们需要找到一个转换公式,使得P22转换回P60。 FOV与世界坐标的关系 视锥体的高度(在深度为z处)与FOV通过以下公式相关: $$ Heightz=2×z×tan⁡(FOV2)\text{Height}_z = 2 \times z \times \tan\left(\frac{FOV}{2}\right)Heightz=2×z×tan(2FOV) $$ 其中: Height_z :在深度为z处的视锥体高度。 FOV:垂直视野角度。 z:深度(从摄像机到目标点的距离)。 视锥体的宽度和高度成比例地变化,具体公式为: $$ Widthz=Heightz×Aspect Ratio\text{Width}_z = \text{Height}_z \times \text{Aspect Ratio}Widthz=Heightz×Aspect Ratio $$ 对于两个不同的FOV(如60度和22度),可以计算出它们对应的视锥体高度比值。为了让FOV为22度时生成的世界坐标与FOV为60度时生成的一致,需要应用这个比值进行缩放。 缩放因子的推导 在FOV为60度时的视锥体高度: $$ \mathrm{Heigth}_{60}=2\times \mathrm{z}\times \tan \left( \frac{60^o}{2} \right) $$ 在FOV为22度时的视锥体高度: $$ \mathrm{Heigth}_{22}=2\times \mathrm{z}\times \tan \left( \frac{22^o}{2} \right) $$ 缩放因子: 需要将FOV为22度时的世界坐标缩放到FOV为60度的视锥体高度,因此缩放因子是这两个视锥体高度的比值: $$ ScaleFactor=\frac{Height_{60}}{Height_{22}}=\frac{\tan \left( 30^{o} \right)}{\tan \left( 11^{o} \right)} $$ 可以计算出这个缩放因子: $$ \mathrm{ScaleFactor}=\frac{0.5774}{0.1944}\approx 2.97 $$ 公式应用 在FOV为22度时生成的3D世界坐标P22可以通过如下公式转换为FOV为60度时的3D世界坐标P60:

Unity3D中根据深度值重建像素点对应的世界坐标

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com 1 深度图介绍 就是将深度信息(Z坐标值)保存在了一张贴图上的R通道上,因为R通道的值范围是[0, 1],所以我们可以用ndc空间下的Z坐标值做下处理就能变成[0, 1]范围了( (Zndc+1)*0.5 2 C#接口取得深度图纹理的方法 Shader.GetGlobalTexture("_CameraDepthTexture") 3 在NDC空间下深度值的计算公式 4 从片元着色器中根据深度图反推出片元世界坐标且输出的shader Shader "zznewclear13/DepthToPositionShader" { Properties { [Toggle(REQUIRE_POSITION_VS)] _Require_Position_VS("Require Position VS", float) = 0 } HLSLINCLUDE #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/core.hlsl" #pragma multi_compile _ REQUIRE_POSITION_VS sampler2D _CameraDepthTexture; struct Attributes { float4 positionOS : POSITION; float2 texcoord : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 texcoord : TEXCOORD0; }; Varyings Vert(Attributes input) { Varyings output = (Varyings)0; // 根据顶点在其模型坐标系下的值,计算得到在裁剪空间中的齐次坐标 VertexPositionInputs vertexPositionInputs = GetVertexPositionInputs(input.

不使用MeshRenderer去绘制Mesh【翻译】

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com 原文地址 Overview (概述) Use DrawMesh in situations where you want to draw large amount of meshes, but don’t want the overhead of creating and managing game objects.DrawMesh draws a mesh for one frame. The mesh will be affected by the lights, can cast and receive shadows, just like if it was part of some game object. It can be drawn for all cameras or just for some specific camera (either by passing a Camera to the call or by calling these methods on a CommandBuffer).