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

Table of Contents

1 Nick Darnell的Hiz算法总结和分析笔记

基于层次化Z缓冲区的遮挡拣选Hierarchical Z-buffer occlusion culling)的实现方式有多种,其中有一种是基于论文Real-Time Rendering (Section 3.3.3)

1.1 层次化Z缓冲区(Hierarchical Z-buffer,HiZ)剔除步骤

  1. 让美术准备好用来遮挡住其他物体的遮挡体occlusion geometry
  2. CPU端:获取到所有的遮挡体,且将在视截体之外的给剔除掉
  3. GPU端:把上一步剔除剩下的遮挡体,将其深度值渲染到深度缓冲区D-RT0
  4. GPU端:对D-RT0进行采样,并填充整个mipchain
  5. CPU端:对当前场景中的所有可见物体,获取到其物体的包围球
  6. GPU端:把上一步获取到的物体包围球,计算该包围球对应的屏幕空间宽度,然后使用该宽度计算 mip 级别,从步骤 4 中生成HiZ 贴图中进行采样
  7. 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.
float DistanceToPlane( float4 vPlane, float3 vPoint )
{
    return dot(float4(vPoint, 1), vPlane);
}
 
// Frustum cullling on a sphere. Returns > 0 if visible, <= 0 otherwise
float CullSphere( float4 vPlanes[6], float3 vCenter, float fRadius )
{
   // 取出包围球球心vCenter到视截体01平面,即上下平面的距离值的小值
   // 取出包围球球心vCenter到视截体23平面,即左右平面的距离值的小值
   // 取出包围球球心vCenter到视截体45平面,即前后平面的距离值的小值
   // 然后三个最小值中的最小值,和包围球的半径相加,大于0者表示球在视截体内,可视,小于等于0者表示在外,不可视
   float dist01 = min(DistanceToPlane(vPlanes[0], vCenter), DistanceToPlane(vPlanes[1], vCenter));
   float dist23 = min(DistanceToPlane(vPlanes[2], vCenter), DistanceToPlane(vPlanes[3], vCenter));
   float dist45 = min(DistanceToPlane(vPlanes[4], vCenter), DistanceToPlane(vPlanes[5], vCenter));
 
   return min(min(dist01, dist23), dist45) + fRadius;
}

// SV_GroupID:	表示当前工作组在整个调度网格中的ID
//				该ID唯一标识调度网格中的每个工作组。
//				用于在全局范围上确定工作组的唯一位置,
//				例如在调度多个工作组时, SV_GroupID可以帮助定位某个工作组的全局位置。
[numthreads(1, 1, 1)]
void CSMain(uint3 GroupId			: SV_GroupID,
			uint3 DispatchThreadId	: SV_DispatchThreadID,
			uint GroupIndex			: SV_GroupIndex)
{
	// Calculate the actual index this thread in this group will be reading from.
	// SV_DispatchThreadID语意,表示当前线程在所属的工作组内的局部ID
	// 如果在C#层,Dispatch(kernelIndex, 10, 10, 1) 被调用,
	// 并且在计算着色器中定义了numthreads(8, 8, 1)
	// 那么 SV_DispatchThreadID 将会表示 从0到79的X和Y维度上所有线程的全局ID。
	int index = DispatchThreadId.x;

	// 取到第index个物体包围球的球心和半径Bounding sphere center (XYZ) and radius (W), world space
	float4 Bounds = Buffer0[index];
 
	// 拣选出在视野之内的包围球
	float fVisible = CullSphere(FrustumPlanes, Bounds.xyz, Bounds.w);

	if (fVisible > 0)
	{
		// 拿到摄像机的坐标,算出摄像机到球心的距离
		float3 viewEye = -View._m03_m13_m23;
		float CameraSphereDistance = distance( viewEye, Bounds.xyz );
		float3 viewEyeSphereDirection = viewEye - Bounds.xyz;

		float3 viewUp = View._m01_m11_m21;
		float3 viewDirection = View._m02_m12_m22;
		float3 viewRight = normalize(cross(viewEyeSphereDirection, viewUp));

		// Help handle perspective distortion.
		// http://article.gmane.org/gmane.games.devel.algorithms/21697/
		float fRadius = CameraSphereDistance * tan(asin(Bounds.w / CameraSphereDistance));

		// Compute the offsets for the points around the sphere
		float3 vUpRadius = viewUp * fRadius;
		float3 vRightRadius = viewRight * fRadius;

		// Generate the 4 corners of the sphere in world space.
		// 算出包围球的外接包围盒的四个顶点
		float4 vCorner0WS = float4( Bounds.xyz + vUpRadius - vRightRadius, 1 ); // Top-Left
		float4 vCorner1WS = float4( Bounds.xyz + vUpRadius + vRightRadius, 1 ); // Top-Right
		float4 vCorner2WS = float4( Bounds.xyz - vUpRadius - vRightRadius, 1 ); // Bottom-Left
		float4 vCorner3WS = float4( Bounds.xyz - vUpRadius + vRightRadius, 1 ); // Bottom-Right

		// Project the 4 corners of the sphere into clip space
		// 变换4个角的坐标,从世界坐标变换到裁剪坐标
		float4 vCorner0CS = mul(ViewProjection, vCorner0WS);
		float4 vCorner1CS = mul(ViewProjection, vCorner1WS);
		float4 vCorner2CS = mul(ViewProjection, vCorner2WS);
		float4 vCorner3CS = mul(ViewProjection, vCorner3WS);
		
		// Convert the corner points from clip space to normalized device coordinates
		// 再把四个角的坐标从裁剪空间变换到NDC
		float2 vCorner0NDC = vCorner0CS.xy / vCorner0CS.w;
		float2 vCorner1NDC = vCorner1CS.xy / vCorner1CS.w;
		float2 vCorner2NDC = vCorner2CS.xy / vCorner2CS.w;
		float2 vCorner3NDC = vCorner3CS.xy / vCorner3CS.w;
		vCorner0NDC = float2( 0.5, -0.5 ) * vCorner0NDC + float2( 0.5, 0.5 );
		vCorner1NDC = float2( 0.5, -0.5 ) * vCorner1NDC + float2( 0.5, 0.5 );
		vCorner2NDC = float2( 0.5, -0.5 ) * vCorner2NDC + float2( 0.5, 0.5 );
		vCorner3NDC = float2( 0.5, -0.5 ) * vCorner3NDC + float2( 0.5, 0.5 );
		
		// In order to have the sphere covering at most 4 texels, we need to use
		// the entire width of the rectangle, instead of only the radius of the rectangle,
		// which was the original implementation in the ATI paper, it had some edge case
		// failures I observed from being overly conservative.
		// 为了让球体覆盖最多 4 个纹素,我们需要使用矩形的整个宽度,
		// 而不是仅使用矩形的半径,这是 ATI 论文中的原始实现,我观
		// 察到它由于过于保守而导致了一些边缘情况失败。
		float fSphereWidthNDC = distance( vCorner0NDC, vCorner1NDC );
		
		// Compute the center of the bounding sphere in screen space
		// 在屏幕空间中计算出包围球球心
		float3 Cv = mul( View, float4( Bounds.xyz, 1 ) ).xyz;
		
		// compute nearest point to camera on sphere, and project it
		// 计算球体上距离相机最近的点,并将其投影
		float3 Pv = Cv - normalize( Cv ) * Bounds.w;
		float4 ClosestSpherePoint = mul( Projection, float4( Pv, 1 ) );
		
		// Choose a MIP level in the HiZ map.
		// The original assumed viewport width > height, however I've changed it
		// to determine the greater of the two.
		//在 HiZ 图中选择一个 MIP 级别。
		// 最初假设视口宽度 > 高度,但我已将其更改为确定两者中较大的一个。
		// This will result in a mip level where the object takes up at most
		// 2x2 texels such that the 4 sampled points have depths to compare
		// against.
		// 这将导致 mip 级别,其中对象最多占用 2x2 个纹素,以便 4 个采样点具有可供比较的深度。
		float W = fSphereWidthNDC * max(ViewportSize.x, ViewportSize.y);
		float fLOD = ceil(log2( W ));

		// fetch depth samples at the corners of the square to compare against
		// 采样
		float4 vSamples;
		vSamples.x = HizMap.SampleLevel( HizMapSampler, vCorner0NDC, fLOD );
		vSamples.y = HizMap.SampleLevel( HizMapSampler, vCorner1NDC, fLOD );
		vSamples.z = HizMap.SampleLevel( HizMapSampler, vCorner2NDC, fLOD );
		vSamples.w = HizMap.SampleLevel( HizMapSampler, vCorner3NDC, fLOD );
		
		float fMaxSampledDepth = max( max( vSamples.x, vSamples.y ), max( vSamples.z, vSamples.w ) );
		float fSphereDepth = (ClosestSpherePoint.z / ClosestSpherePoint.w);
		
		// cull sphere if the depth is greater than the largest of our HiZ map values
		BufferOut[index] = (fSphereDepth > fMaxSampledDepth) ? 0 : 1;
	}
	else
	{
		// The sphere is outside of the view frustum
 		BufferOut[index] = 0;
	}
}

1.4 Hierarchical Z-Buffer Occlusion Culling - Shadows

Stephen Hill解释了如何使用分层Z缓冲区遮挡剔除算法从场景中剔除阴影。他们参考了论文CC Shadow Volumes的一些思想,但解决方案更为简单。

剔除掉阴影的方式和步骤如下:

  1. 从玩家视角创建Hi-Z buffer,命名为Player-HZB
  2. 从产生阴影的透射光的视角创建Hi-Z buffer,命名为Light-HZB
  3. 使用Light-HZB剔除投射阴影的物体。如果物体被剔除,则将其标记为被剔除。
  4. 如果对象未被剔除,需要计算其阴影体积范围的粗略近似值。可以将此体积视为包围盒 (A-D) 上的最小深度点和从这些点投射到Light-HZB (E-H) 上的最大采样深度(如下面的图1所示)。有了阴影的bouding volume,可以将该volume变换到玩家视角的观察空间,并根据Player-HZB对其进行测试(如下面的图2所示)。如果您看不到体积,表示该产生阴影的物体被剔除,否则将其视为可见。

图1

图2




2 Mike Turizin的《Hierarchical Depth Buffer》读书笔记

2.1 概述

分层深度(Z)缓冲区(Hierarchical depth(Z) buffer)是一种多级深度 (Z) 缓冲区(multi-level depth (Z) buffer ),通常简称为Hi-Z。使用此技术可以加速深度值查询。与普通纹理 mipmap 一样,每个mipmap级别的尺寸,通常是全分辨率缓冲区尺寸的连续2次幂分数。接下来介绍了两种从全分辨率缓冲区生成分层深度缓冲区的技术:

  1. 首先将展示如何为深度缓冲区生成完整的mipmap,以便即使深度缓冲区的高宽不是2的幂,也能在纹理坐标 (或 NDC) 空间中保持深度查询的准确性。
  2. 其次对于只需要一个降采样级别(a single downsampled level)的情况,将展示如何使用调用单个计算着色器来生成此级别,该调用使用工作组共享内存中的原子操作。这会提速很多。

2.2 介绍

分层式深度缓冲区可以用在加速遮挡剔除accelerate occlusion culling)、屏幕空间反射screen-space reflections)、屏幕空间环境光遮蔽screen-space ambient occlusion)、体积雾volumetric fog)等的实现。

Hi-Z的总体思路是”通过从低分辨率缓冲区读取数据来加速深度查询“。这比从全分辨率的深度缓冲区读取数据更快,这是因为:

  1. 低分辨率reduced-resolution)缓冲区中的单个纹素(或仅仅几个纹素),可代替全分辨率缓冲区的多个纹素。
  2. 低分辨率的缓冲区可以足够小,以适合存放在快速缓存中,从而可以加快查找,特别是加快随机访问查找random-access lookup)。

一般地,在某一级别mipmap下的Hiz缓冲区中的某个纹素,存储的是它从上一个mipmap级别中归纳(subsume)出来的所有纹素的极大值或者极小值,或者两者都存储。而不是存储归纳出来的所有纹素的算术平均值。这是因为平均深度对于需要的查询类型基本无用。

Hi-Z 缓冲区最常见的查询方式是提前退出(early out),即已知条件不满足的情况下,避免进一步处理。尤其是在全分辨率缓冲区中进行更精确的查找。如果深度缓冲区是一个非反向式(non-reversed)的深度缓冲区,即深度值越大,表示离当前摄像机越远,最远处为1。这样子可以快速确定屏幕空间位置是否确实被深度缓冲区遮挡——如果待判断的片元,其Z值大于Hi-Z中的纹素值,则表示该片元点落在HiZ遮挡之后,表示被遮挡了。

2.3 技术 1:生成完整的 Mip 链

许多 Hi-Z 应用程序需要完整的深度缓冲区 mipmap 链。例如,Hi-Z 遮挡剔除的工作原理是:把物体的包围盒投影到屏幕空间,并使用投影的大小来选择合适的 mipmap 级别,这样子以便每个遮挡测试访问固定数量的纹素

从全分辨率深度缓冲区生成 mipmap链是很简单的:对于级别 N 中的每个纹素,获取先前生成的级别 N-1 中相应的 4 个纹素 的最大值或者最小值。连续执行此过程(每次将两个维度减半),直到生成最终的 1x1 mip 级别。 但是,对于非2的幂维度的深度缓冲区,情况要复杂得多。因为需要按屏幕分辨率的尺寸构建 Hi-Z缓冲区(屏幕分辨率的尺寸通常很少是 2 的幂),所以需要考虑上这种非2的幂的情况。

首先确定深度缓冲区 mipmap 级别的每个纹素中的值应该表示什么。在此,假设mipmap中存储的是上一级的4个纹素的最小值。

So when we get the value of a particular texel in mip level N, what does its value signify, precisely? It should be the min of all texels in the full-res depth buffer that occupy the same footprint in (normalized) texture coordinate space. 那么,当我们在 mip 级别 N 中获取特定纹素的值时,它的值究竟意味着什么?它应该是 min 全分辨率深度缓冲区中所有纹素的,这些纹素在 (规范化) 纹理坐标空间中占用相同的占用空间。

uniform sampler2D u_depthBuffer;
uniform int u_previousLevel;
uniform ivec2 u_previousLevelDimensions;

void main() 
{
	// https://zhuanlan.zhihu.com/p/102068376
	// gl_FragCoord 表示当前片元着色器处理的候选片元窗口相对坐标信息,
	// 是一个 vec4 类型的变量 (x, y, z, 1/w), 其中 x, y 是当前片元的窗口坐标,
	// OpenGL 默认以窗口左下角为原点, 在着色器中通过布局限定符可以重新设定原点,
	// 比如窗口左上角为原点 origin_upper_left,窗口大小由 glViewport() 函数指定。
	// x, y 默认是像素中心而非整数,原点的窗口坐标值为 (0.5, 0.5), 小数部分恒为0.5,
	// 当viewport 范围 为(0,0,800,600)时, x, y 的取值范围为
	//(0.5, 0.5, 799.5, 599.5), 当在着色器中布局限定符设置为 pixel_center_integer 时,
	// x, y 取值为整数。第三个分量 z 表示的是当前片元的深度信息,由 vertex shader 处理过后系统插值得到
	
	ivec2 thisLevelTexelCoord = ivec2(gl_FragCoord);
	ivec2 previousLevelBaseTexelCoord = 2 * thisLevelTexelCoord;

	// 在GLSL环境要想精确地换取每个像素的值,这个时候就不能使用传统的 texture(sampler2d) ,
	// 因为采样器sampler2d默认是float精度,而且texture采用函数会涉及归一化、过滤以及插值
	// 等复杂操作,基本无法得到某一确切像素的值。texlFetch 是 OpenGL ES 3.0 引入的 API ,
	// 它将纹理视为图像,可以精确访问像素的内容,我们可以类比通过索引来获取数组某个元素的值。
	// texelFetch 使用的是未归一化的坐标直接访问纹理中的纹素,不执行任何形式的过滤和插值操作,
	// 坐标范围为实际载入纹理图像的宽和高。
    // https://blog.csdn.net/wyq1153/article/details/126191318
	vec4 depthTexelValues;
	depthTexelValues.x = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord,
                                    u_previousLevel).r;
	depthTexelValues.y = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(1, 0),
                                    u_previousLevel).r;
	depthTexelValues.z = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(1, 1),
                                    u_previousLevel).r;
	depthTexelValues.w = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(0, 1),
                                    u_previousLevel).r;

	float minDepth = min(min(depthTexelValues.x, depthTexelValues.y),
                         min(depthTexelValues.z, depthTexelValues.w));

    // Incorporate additional texels if the previous level's width or height (or both)
    // are odd. 判断上一级的mipmap的高宽是不是奇数值
	bool shouldIncludeExtraColumnFromPreviousLevel = ((u_previousLevelDimensions.x & 1) != 0);
	bool shouldIncludeExtraRowFromPreviousLevel = ((u_previousLevelDimensions.y & 1) != 0);
	
	// 上一级的mipmap是奇数列
	if (shouldIncludeExtraColumnFromPreviousLevel)
	{
		vec2 extraColumnTexelValues;
		extraColumnTexelValues.x = texelFetch(u_depthBuffer,
                                              previousLevelBaseTexelCoord + ivec2(2, 0),
                                              u_previousLevel).r;
		extraColumnTexelValues.y = texelFetch(u_depthBuffer,
                                              previousLevelBaseTexelCoord + ivec2(2, 1),
                                              u_previousLevel).r;

		// In the case where the width and height are both odd, need to include the
        // 'corner' value as well.
		if (shouldIncludeExtraRowFromPreviousLevel) 
		{
			float cornerTexelValue = texelFetch(u_depthBuffer,
                                                previousLevelBaseTexelCoord + ivec2(2, 2),
                                                u_previousLevel).r;
			minDepth = min(minDepth, cornerTexelValue);
		}
		
		minDepth = min(minDepth, min(extraColumnTexelValues.x, extraColumnTexelValues.y));
	}
	if (shouldIncludeExtraRowFromPreviousLevel) 
	{
		vec2 extraRowTexelValues;
		
		extraRowTexelValues.x = texelFetch(u_depthBuffer,
                                           previousLevelBaseTexelCoord + ivec2(0, 2),
                                           u_previousLevel).r;
		
		extraRowTexelValues.y = texelFetch(u_depthBuffer,
                                           previousLevelBaseTexelCoord + ivec2(1, 2),
                                           u_previousLevel).r;
		
		minDepth = min(minDepth, min(extraRowTexelValues.x, extraRowTexelValues.y));
	}

	// gl_FragDepth是一个float类型的变量,它表示对应像素的z值。这个z值是在纹理空间中的z值,也就是深度值。
	gl_FragDepth = minDepth;
}

To put it another way, if a particular texture coordinate (in [ 0 , 1 ] 2 [0,1] 2 ) maps (using nearest-neighbor filtering) to a particular texel in the full-res buffer, then that full-res texel must be considered for the min that is computed for the texel in each higher mip level that the same texture coordinate also maps to. 换句话说,如果特定纹理坐标 (in [ 0 , 1 ] 2 [0,1] 2 ) 映射 (使用最近邻筛选) 到全分辨率缓冲区中的特定纹素,则必须考虑该全分辨率纹素,以便在同一 min 纹理坐标也映射到的每个较高 mip 级别中为纹素计算。

When this correspondence is guaranteed, we know that a lookup in the higher mip levels will never yield a depth value that is > the value of the texel the same texture coordinate maps to in the full-res buffer (level 0). For a particular level N, this guarantee also holds true for all levels under that level (< N). 当保证这种对应关系时,我们知道在较高 mip 级别中查找永远不会产生深度值,该值>全分辨率缓冲区 (级别 0) 中相同纹理坐标映射到的纹素的值。对于特定的级别 N,此保证也适用于该级别 (< N) 下的所有级别。

For even level dimensions (which for power-of-2-dimensioned full-res buffers will be the case for every level until a dimension becomes 1), this is easy. In the one-dimensional case, for a texel in level N with index i i, we need to consider the texels in level N-1 with indices 2 i 2i and 2 i + 1 2i+1 for the min. Thus, D N [ i ]

min ( D N − 1 [ 2 i ] , D N − 1 [ 2 i + 1 ] ) D N ​ [i]=min(D N−1 ​ [2i],D N−1 ​ [2i+1]). There is a direct 2-to-1 mapping between texels (and thus texture coordinate footprints) because the dimension of each level is exactly half of the previous. 对于偶数级维度(对于 2 维的幂全分辨率缓冲区,每个级别都是如此,直到维度变为 1),这很容易。在一维情况下,对于索引为 的级别 N 中的纹素 i i ,我们需要考虑具有索引的级别 N-1 中的纹素 2 i 2i ,对于 2 i + 1 2i+1 min .因此, D N [ i ]

min ( D N − 1 [ 2 i ] , D N − 1 [ 2 i + 1 ] ) D N ​ [i]=min(D N−1 ​ [2i],D N−1 ​ [2i+1]) .纹素之间有直接的 2 对 1 映射(因此纹理坐标足迹),因为每个级别的尺寸正好是前一个级别的一半。

Example for even level dimensions: The 6 texels in this level reduce to 3 in the next level up. The texture coordinate footprints of the (3) higher-level texels each overlap exactly 2 texels from the lower level. (Circles are texel centers, and the boxes around them represent texture coordinate footprints when using nearest-neighbor filtering.) Example for even level dimensions: The 6 texels in this level reduce to 3 in the next level up. The texture coordinate footprints of the (3) higher-level texels each overlap exactly 2 texels from the lower level. (Circles are texel centers, and the boxes around them represent texture coordinate footprints when using nearest-neighbor filtering.) 偶数级尺寸示例:此级别中的 6 个纹素在下一个级别中减少到 3 个。(3) 个较高级别的纹素的纹理坐标足迹每个与较低级别正好重叠 2 个纹素。(圆圈是纹素中心,它们周围的框表示使用最近邻筛选时的纹理坐标足迹。

For odd level dimensions (of which there will be at least one for non-power-of-2 full-res dimensions), things are more complicated. For a level N-1 of odd dimension d i m N − 1 dim N−1 ​ , the dimension of the next highest level (N) will be d i m N

⌊ d i m N − 1 2 ⌋ dim N ​ =⌊ 2 dim N−1 ​

​ ⌋, which is ≠ d i m N − 1 2  ​

2 dim N−1 ​

​ . 对于奇数级维度(其中至少有一个非 2 次方全分辨率维度),情况要复杂得多。对于奇数维度 d i m N − 1 dim N−1 ​ 的级别 N-1 ,下一个最高级别 (N) 的维度将为 d i m N

⌊ d i m N − 1 2 ⌋ dim N ​ =⌊ 2 dim N−1 ​

​ ⌋ ,即 ≠ d i m N − 1 2  ​

2 dim N−1 ​

​ 。

What this means is that there is no longer a straightforward 2-to-1 mapping of texels in level N-1 to level N. Instead, the texture-coordinate-space footprint of each texel in level N overlaps the footprint of 3 texels in level N-1. 这意味着,从 N-1 级别到 N 级别中,纹素不再有简单的 2 对 1 映射。相反,级别 N 中每个纹素的纹理坐标空间占用空间与级别 N-1 中 3 个纹素的占用空间重叠。

Example for odd level dimensions: The 7 texels in this level reduce to 3 in the next level up. The texture coordinate footprints of the (3) higher-level texels each overlap the footprints of 3 texels from the lower level. Example for odd level dimensions: The 7 texels in this level reduce to 3 in the next level up. The texture coordinate footprints of the (3) higher-level texels each overlap the footprints of 3 texels from the lower level. 奇数级尺寸示例:此级别中的 7 个纹素在下一级别减少到 3 个。(3) 个较高级别纹素的纹理坐标足迹每个与来自较低级别的 3 个纹素的足迹重叠。

Thus, D N [ i ]

min ( D N − 1 [ 2 i ] , D N − 1 [ 2 i + 1 ] , D N − 1 [ 2 i + 2 ] ) D N ​ [i]=min(D N−1 ​ [2i],D N−1 ​ [2i+1],D N−1 ​ [2i+2]). This means that the same texel in level N-1 sometimes contributes to the min computed for 2 texels in level N. This is necessary to maintain the correspondence earlier described. 因此, D N [ i ]

min ( D N − 1 [ 2 i ] , D N − 1 [ 2 i + 1 ] , D N − 1 [ 2 i + 2 ] ) D N ​ [i]=min(D N−1 ​ [2i],D N−1 ​ [2i+1],D N−1 ​ [2i+2]) .这意味着级别 N-1 中的相同纹素有时会影响级别 N 中 2 个纹素 min 的计算。这对于保持前面描述的对应关系是必要的。

The above description was done in one dimension for simplicity. For two dimensions, if both dimensions of level N-1 are even, a 2x2 texel region in level N-1 maps to a single texel in level N. If one dimension is odd, then either a 2x3 or 3x2 region in level N-1 maps to a single texel in level N. If both dimensions are odd, then the “corner” texel shared by the row and column extensions must also be considered—so a 3x3 region in level N-1 maps to a single texel in level N. 为简单起见,上述描述是在一个维度中完成的。对于两个维度,如果级别 N-1 的两个维度是偶数,则级别 N-1 中的 2x2 纹素区域将映射到级别 N 中的单个纹素。如果一个维度为奇数,则级别 N-1 中的 2x3 或 3x2 区域映射到级别 N 中的单个纹素。如果两个维度都是奇数,则还必须考虑行和列扩展共享的“角”纹素,因此 N-1 级中的 3x3 区域映射到 N 级中的单个纹素。

参考资料

kumakoko avatar
kumakoko
pure coder
comments powered by Disqus