游戏引擎学习第93天

devtools/2025/2/11 15:08:17/

回顾并推动进展

我们上一期的进展是,我们第一次开始填充可以任意旋转和缩放的固体形状。接下来我们希望继续推进图形功能,能够支持更多的特性,以便拥有一个真正的渲染器。

目前的目标是开始填充这些形状,并能够支持旋转、缩放,甚至可能是非均匀缩放。我们希望能够在不同的方向上进行不同的缩放,比如让物体变成椭圆形。但是,我们不会支持扭曲这类操作,因为当前的需求并不需要,所以我们将专注于这种简单的操作。

接下来,我们希望将这些形状从单一的颜色填充,转变为可以映射纹理的形状。目标是实现纹理映射,能够在这些旋转和缩放的图形上显示图片,例如在头部或躯干上放置纹理。通过这种方式,我们将能够将任何图像映射到这些旋转和缩放的多边形上。

今天的讨论将从黑板上开始,讲解纹理映射的相关内容,然后我们将开始实现它。接下来的一周,我们会继续推进并完成这些内容。
在这里插入图片描述

扩展坐标系统的概念,以允许传递纹理

接下来,我们希望在图形渲染过程中能够传递纹理信息。首先,我们需要扩展当前的坐标系统,这个坐标系统目前用于测试和传递各种渲染参数。为了支持纹理映射,我们需要将纹理图像传递给渲染系统。这个纹理图像将用于填充图形,可能是游戏中的某个物体,比如树木。

具体来说,我们在游戏状态结构体中会加载一些资源,假设我们用树作为示例,然后在绘制树时,我们会传递加载的位图(即纹理),并将它存储在渲染状态中。这样,渲染系统就知道该使用哪一张纹理来填充图形。

接下来,我们需要确保在调用 DrawRectangleSlowly 时,将纹理作为参数传递进去,使得该函数能够使用正确的纹理来填充矩形。这是下一步的工作,涉及到将纹理传递到渲染函数,并确保图形能够正确显示纹理内容。

在这里插入图片描述

复习我们的边缘方程

目前,图形渲染的核心部分是通过边界方程来判断是否处于形状的内部,这决定了是否需要为该像素赋予颜色或让其保持透明。当前的实现方法使用的是简单的边界框优化,通过检测边界框内的所有像素来加速填充过程,这比直接遍历整个屏幕要高效得多。

但是,现阶段的优化方法仍然较为粗糙,仅通过边界框来快速筛选像素,未来可能需要进一步改进算法,避免考虑那些距离实际区域很远的像素,从而提升效率。

接下来的任务是将填充颜色的部分从固定的颜色(例如原本传递的32位颜色)替换为从纹理中读取的颜色。换句话说,当前的颜色填充逻辑将被修改,改为从纹理中采样颜色,并使用该颜色填充像素。这意味着,纹理映射的实现需要与现有的像素填充逻辑兼容。

在这里插入图片描述

黑板:从基本原理解决问题

接下来,讨论如何解决当前的问题。将不会直接引导大家到达最终的解决方案,而是展示如何从问题出发,逐步思考并尝试找到一个解决办法。许多问题都有过长期的研究,现有的解决方案可能已经非常成熟,但本次目标是展示解决问题的过程,并帮助大家理解如何处理没有现成答案的情况。

即使当前的方案可能不是最终最优解,也会通过这个过程帮助大家理解如何从头开始分析问题,并找到可行的初步解决办法。尽管最终可能会借助研究论文或其他成熟的方法,但这次重点是学习如何有效地分析问题并逐步推导出答案,而不是直接采用现成的最优解。

因此,讨论将从问题的分析开始,探索解决方案的思路,并按照这一分析逐步推进解决方案的实施。

黑板:纹理映射

当前讨论的是如何将纹理映射到一个矩形区域。首先,确定了坐标系,包括x轴和y轴,并且每个方向上都有缩放。矩形的绘制过程是在这个坐标系中进行的,纹理的位置信息也基于这个坐标系来定义。

接下来,考虑如何将一个规则的矩形像素网格(即纹理)映射到一个矩形区域中。通过对比,我们可以设想将纹理的网格对齐到目标矩形区域的过程。这个过程中需要对纹理进行适当的缩放和旋转,确保它能够完全适配到目标区域。具体来说,在坐标系的底部中心开始,纹理的像素会按照比例对应到目标区域的像素位置。

此时,要处理两个坐标系:一个是实际绘制矩形的像素网格,另一个是纹理的像素网格。这两个网格叠加在一起,形成了纹理映射的基础。通过这种方式,可以确保每个目标区域的像素都对应到纹理网格中的一个像素,进而实现纹理的正确映射。

黑板:标准命名法

在讨论纹理映射时,通常使用的是u、v坐标系。u坐标表示纹理的x方向,v坐标表示纹理的y方向,且它们的值通常在0到1之间。传统上,纹理的x坐标范围是从0到纹理的宽度减去1,而y坐标范围是从0到纹理的高度减去1,这样可以用来查找纹理中的像素。

为了方便理解,可以将纹理的坐标值归一化到0到1之间。这意味着,u、v坐标值代表的是纹理的相对位置,而不是绝对的像素位置。为了将这些归一化的坐标映射回像素坐标,方法很简单:将u、v坐标分别乘以纹理的宽度减去1和高度减去1,得到相应的像素坐标。

u、v坐标系与直接使用纹理的像素坐标(例如纹理的x和y坐标)是可以互换的,二者的映射关系非常直接。通过这种方式,可以很方便地对纹理进行操作,无论是通过u、v坐标,还是通过纹理的像素坐标,最终都能获得正确的纹理数据。这种方法对于理解纹理映射是非常直观且有效的。

黑板:我们如何生成一些 u 和 v 值,以告诉我们在纹理中的位置?

问题是,如何生成u和v坐标值,以便在给定时刻知道纹理中的位置。假设有一个像素网格,表示当前的绘制区域,坐标系统的原点位于屏幕的左下角。我们需要确定每个像素在纹理中的对应位置,也就是确定每个像素应该从纹理中取什么颜色。

要解决这个问题,关键是要知道每个像素在x轴和y轴上占据的比例。首先,假设从原点沿着y轴向上走,当走到某个位置时,y轴上的位置就代表了该像素在y方向上的比例。通过这个比例,我们可以将其归一化到0到1之间,这样就得到了v值。

同样,沿着x轴计算的比例,也可以归一化得到u值。具体来说,沿x轴走到的距离除以x轴的总长度就得到u值,沿y轴走到的距离除以y轴的总长度就得到v值。通过这种方式,u和v坐标值分别表示当前像素在纹理中的位置,范围从0到1。

这个方法通过比例计算,使得每个像素都可以根据其在屏幕上的位置,映射到纹理的正确位置,进而确定每个像素的颜色。

黑板:看看这里会发生什么

首先,我们考虑一个给定的像素位置(记作p)。如果我们想要将这个像素投影到y轴上,我们可以使用点积来计算。点积用来衡量两个向量之间的关系,通常用于投影计算。为了简化,假设我们将原点从p中减去,这样我们就得到了一个新的向量d(代表从原点到p的向量)。

接下来,如果我们将d与y轴向量进行点积,公式如下:

d ⋅ y = ∣ d ∣ × ∣ y ∣ × cos ⁡ ( θ ) d \cdot y = |d| \times |y| \times \cos(\theta) dy=d×y×cos(θ)

其中,|d|和|y|分别是d和y的长度, θ \theta θ是它们之间的夹角。这个结果给出了一个包含长度信息的数值,但实际上我们更希望得到的是一个归一化的值,表示在y轴方向上,d的投影占多大比例。为了解决这个问题,我们可以对这些向量进行归一化。

通过归一化d和y轴向量,我们就得到了长度为1的单位向量。这样,点积的结果就变成了两个单位向量之间夹角的余弦值(cosine),即:

d normalized ⋅ y normalized = cos ⁡ ( θ ) d_{\text{normalized}} \cdot y_{\text{normalized}} = \cos(\theta) dnormalizedynormalized=cos(θ)

这个值就表示了d在y轴上的投影比例,而且由于向量已经被归一化,结果是一个范围在0到1之间的数值,能够告诉我们d在y轴上占的比例。

黑板:这个方程是否能给我们想要的结果?

现在,我们考虑这种方法是否能给出我们想要的值,即v坐标(或者如果是x轴的话,是u坐标)。我们需要确认这种方法是否有效。

另一种思考这个问题的方法是,如果我们只对坐标轴进行归一化,而不改变点的长度。假设我们将d向量和y轴归一化后的结果进行点积,得到的值将是d的长度乘以余弦值,这样就去除了长度的影响。该值可以告诉我们d向量在y轴上的位置,单位是实际的像素距离。

为了确定y轴上距离的绝对位置,首先要将这个值除以y轴的总长度,这样我们就能得到归一化的比例值。实际上,我们希望计算的就是这种归一化后的结果。

如果我们仔细思考,真正需要的步骤是:我们希望将余弦值除以y轴的平方长度。也就是说,实际的步骤应该是先计算出点积结果,然后再通过除以y轴的长度来归一化,从而得到准确的v坐标。

这个方法可以确保我们得到正确的比例,反映点在y轴上的相对位置。

黑板:从另一个角度看问题

为了更清楚地理解整个过程,先重新说明一次。假设我们有一个测量轴,这个轴可以是 x x x轴或 y y y轴,我们假设这个轴的长度是单位长度,即长度为1。

然后,我们知道有一个四边形的边长,但这个边长的实际数值我们并不确定。假设四边形的长度是 l l l l l l的值是四边形的实际长度(比如纹理的长度)。现在我们考虑一个点 p p p,并将这个点投影到这个轴上。为了完成投影,我们先计算出 d d d向量,它是点 p p p与原点之间的差,即 d = p − o r i g i n d = p - origin d=porigin

接下来,通过将 d d d与轴 a a a做点积,得到 d d d与轴 a a a的投影长度。这时候,得到的结果是 d d d的长度乘以 c o s ( θ ) cos(θ) cos(θ),这就是实际的像素距离。然而,接下来我们需要将这个值转换成一个从0到1之间的统一坐标系统。因此,我们需要将这个结果除以 l l l l l l就是轴 a a a的总长度。这样,我们就得到了一个归一化的坐标值,表示点 p p p在该轴上的相对位置。

通过这种方式,我们能够将一个实际的像素位置映射到一个统一的坐标系统中,方便后续的计算和处理。

黑板:我们想要实现的方程

经过分析,最终得出的结论是,所需要的操作是先将 d d d向量与原始轴(即未归一化的 y y y轴)进行点积,得到 d d d的长度、 y y y轴的长度和它们之间的夹角余弦值。然后,计算出点积的结果,并将其除以 y y y轴的长度。这个除法操作会取消掉 y y y轴的长度部分,留下相对位置。

具体来说,首先计算出 d d d y y y轴的点积结果,然后将这个结果除以 y y y轴的长度,再除以一次。这样操作的结果会得到正确的归一化值,即从0到1的比例值。

这就是推导过程中的第一个尝试,虽然在此过程中可能出现了一些小的数学错误,但这个过程已经产生了符合预期的结果,并且是根据研究所得出的合理结论。

根据分析,操作的推导过程可以用以下公式表示:

  1. 首先,计算向量 d d d y y y轴的点积:
    d ⋅ y = ∣ d ∣ ⋅ ∣ y ∣ ⋅ cos ⁡ ( θ ) \mathbf{d} \cdot \mathbf{y} = |\mathbf{d}| \cdot |\mathbf{y}| \cdot \cos(\theta) dy=dycos(θ)
    其中, ∣ d ∣ |\mathbf{d}| d d d d的长度, ∣ y ∣ |\mathbf{y}| y y y y轴的长度, cos ⁡ ( θ ) \cos(\theta) cos(θ) d d d y y y轴之间的夹角余弦值。

  2. 然后,将该点积结果除以 y y y轴的长度 ∣ y ∣ |\mathbf{y}| y,得到归一化的结果:
    d ⋅ y ∣ y ∣ = ∣ d ∣ ⋅ ∣ y ∣ ⋅ cos ⁡ ( θ ) ∣ y ∣ = ∣ d ∣ ⋅ cos ⁡ ( θ ) \frac{\mathbf{d} \cdot \mathbf{y}}{|\mathbf{y}|} = \frac{|\mathbf{d}| \cdot |\mathbf{y}| \cdot \cos(\theta)}{|\mathbf{y}|} = |\mathbf{d}| \cdot \cos(\theta) ydy=ydycos(θ)=dcos(θ)

  3. 接下来,需要对结果进行进一步归一化,将结果从0到1的范围内调整。为了实现这一点,再除以 y y y轴的长度一次:
    ∣ d ∣ ⋅ cos ⁡ ( θ ) ∣ y ∣ \frac{|\mathbf{d}| \cdot \cos(\theta)}{|\mathbf{y}|} ydcos(θ)

  4. 最终,得到的结果就是归一化的值,它表示的是点 d d d y y y轴上的相对位置,范围从0到1。

    最终得到:
    ∣ d ∣ ⋅ ∣ y ∣ ⋅ cos ⁡ ( θ ) ∣ y ∣ 2 \frac{|\mathbf{d}| \cdot |\mathbf{y}| \cdot \cos(\theta)}{|\mathbf{y}|^2} y2dycos(θ)

  • 第一次除以 ∣ y ∣ |\mathbf{y}| y,我们去除了点积结果中与 y y y轴长度的依赖,得到了 d d d y y y轴上的投影。
  • 第二次除以 ∣ y ∣ |\mathbf{y}| y,是为了确保最终结果是标准化的,使得它符合纹理坐标的要求,确保坐标的范围从0到1。

这个推导过程描述了如何通过点积、除以轴的长度来得到归一化值,从而获得正确的纹理坐标的计算方式。

实现这个方程,从边缘测试中分配负号开始

在这个过程中,进行p - origin操作时,可以注意到,如果我们要对这个表达式做反应,可以通过分配负号来实现。如果将负号分配到每个分量,那么就会得到每个分量的负值,最终的结果是每个分量都会变成负值。这种操作会对向量中的每个元素产生影响,且这个过程是简单的负号分配。
在这里插入图片描述

引入 d

如果每次编译时都没有看到变化,那么可以通过去掉负号来简化表达式,这样可以引入我们之前提到的d值。如果将像素p - origin表示为d,就能看到这些操作实际上是相同的。虽然我们没有对它们做进一步简化,但通过这种方式,可以清楚地看到d值已经准备好用于计算。
在这里插入图片描述

计算 u 和 v

要计算纹理坐标,首先需要计算u和v值。首先,只有在实际进入纹理区域时才需要进行这些计算,因为如果已经在纹理区域内,就无需再次进行纹理查找。对于u值,可以通过计算点积来实现。具体来说,需要将d与x轴的单位向量做点积,并将结果乘以x轴长度的倒数的平方。这意味着,最终计算u值时,实际上是在做一个除法操作,相当于将结果除以x轴的长度的平方。

对于v值,则可以做同样的操作,只不过是针对y轴进行计算。最终,u和v就可以组合成一个向量,传递给纹理采样函数进行处理。为了提高效率,可以在计算时预先计算出x轴和y轴的长度倒数平方,并在后续计算u和v时使用这些值。这些计算理论上会得到正确的纹理坐标。
在这里插入图片描述

在纹理中查找 u 和 v

首先讨论的是如何从纹理中查找相应的颜色值。目标是根据纹理坐标(u 和 v)获取相应的像素值。

  1. 计算纹理坐标:

    • 已经获得了 uv,这两个值代表了纹理的坐标。
    • 需要将这些坐标与纹理的宽度和高度相乘,以确定像素在纹理中的位置。
    • 对于 x 坐标,首先需要将 u 乘以纹理的宽度,并减去 1,确保可以正确地获取到纹理的最后一个像素。
    • 同样,对于 y 坐标,v 会与纹理的高度相乘。
  2. 范围检查:

    • 通过调试检查确保 uv 坐标在合法范围内,即它们应该在 [0, 1] 的区间内。
    • 在计算坐标时,要确保它们不会超出纹理的边界。
  3. 四舍五入:

    • 在进行纹理查找时,需要对坐标进行四舍五入。为了确保精确的纹理坐标,使用了加上 0.5 进行四舍五入的方法。
    • 这样可以确保纹理的坐标值是准确的,避免因浮动值导致的不精确像素选择。
  4. 纹理查找:

    • 在计算出最终的 xy 坐标后,使用这些坐标来从纹理中获取相应的颜色值。
    • 通过将这些坐标与纹理的实际宽度和高度进行结合,可以精确地定位并获取正确的纹理像素。

通过以上步骤,可以确保根据 uv 坐标从纹理中准确地查找到对应的像素颜色,并进行适当的调试和四舍五入处理,避免出现越界问题。
在这里插入图片描述

编译并查看游戏中的效果

在这一过程中,首先对计算出的 uv 进行了检查,确保它们不会超出合法范围。当前的编译结果表明,uv 的值都保持在合法范围内,这意味着没有发生越界问题。这是一个好兆头,表明坐标计算和纹理查找的过程没有出现错误或不一致。

确保 X 和 Y >=0 且 < 纹理尺寸

在这个过程中,同样对 x 坐标进行了检查,确保其大于等于零并小于纹理宽度。这保证了在纹理查找时不会发生越界错误。经过这样的检查,当前的结果表明,所有坐标都在合法范围内,因此没有发生越界问题,大家都对此感到满意。
在这里插入图片描述

查找 u 和 v

在这个过程中,首先通过指针指向纹理的基址,然后根据 y 坐标乘以纹理的 pitch 来定位到正确的行。接着,根据 x 坐标的偏移量,即像素的大小(每个像素占用 4 字节),进一步定位到正确的像素。这样就可以得到目标纹理中的正确像素。

最终,通过该指针读取像素的 RGB 值和 alpha 通道的值,并将其存储到指定的位置。这样就完成了纹理的查找和数据读取过程。
在这里插入图片描述

在这里插入图片描述

查看游戏中的纹理映射效果

经过实现,纹理映射已经完成,结果是一个非常基础的实现,并没有涉及复杂的处理。纹理映射本身非常简单,第一轮实现没有做任何特别复杂的工作。通过进一步观察,得出这是一个基本的纹理映射流程。

此外,若需要,可以在这个过程中加入 alpha 混合功能,甚至可以实现 alpha 剪切。虽然目前没有进行优化处理,也没有涉及到更精细的部分,但纹理映射的基本流程已经能够顺利运行,完成了目标功能。

加入 Alpha

在现有的纹理映射基础上,加入了 alpha 混合的功能。具体操作是先从之前的代码中获取处理 alpha 的部分,并将其嵌入当前的纹理映射逻辑中。在进行纹理查找之后,需要确保从正确的像素位置读取数据,之后对该像素应用 alpha 混合。最终的 texel 值与 alpha 值结合,生成最终的颜色值。

此时,alpha 值将用于调制颜色,并且通过对该值的处理,确保混合效果得以正确实现。整个过程的修改已完成,并已保存,目标是让 alpha 值能够正确影响最终颜色的显示。
在这里插入图片描述

在这里插入图片描述

在游戏中看到 Alpha 合成效果

现在,alpha 合成也已正确实现,意味着纹理映射与之前在位图中实现的所有特性都正常工作了。这是一个很好的进展。接下来可能会涉及一些更复杂的内容,但目前已经达成了预期目标,可以选择暂停此阶段,保持现有成果。

放慢花哨效果

接下来,首先需要减慢速度,做一些准备工作,以便更好地理解接下来需要进行的操作及其原因。因此,决定在这一阶段保持简单,并确保一切都保持统一的大小,而不急于让其变得过于复杂。

触发断言,推迟直到使用 SIMD 时才对 u 和 v 进行限制

在这个过程中,出现了一个色调超出了精度的情况。虽然这种情况是可以预见的,但它需要被限制(clamp)或者修改计算规则。不过,大多数情况下,这不算是一个实际的问题,因为即使u值稍微超出范围,也不会导致越界。最终,我们可以通过限制这些值来解决这个问题,实现起来也相对简单,所以这并不是一个大问题。

继续放慢花哨效果

在这里,目标是修正当前的设置,确保各个部分的尺寸保持一致。为了让效果更明显,将尺寸增大,并且尽管这会使得渲染变得更慢,但可以更清楚地看到变化的效果。接下来,计划将角度的变化速度调整得更慢,以便更容易观察变化的过程。
在这里插入图片描述

在这里插入图片描述

注意到闪烁并提到亚像素精度

在当前的渲染中,出现了明显的闪烁问题,这是因为每一步都在使用较大的像素块,这使得渲染效果不够平滑,也没有考虑到亚像素级别的精度。为了改善这一点,接下来计划引入亚像素的精度处理,虽然这会涉及更多的内容,但可以从现在开始进行探索,并在未来进一步完善这一部分。
在这里插入图片描述

放大树并将其置于屏幕中心

为了改善显示效果,计划将对象放大并将其居中显示在屏幕上。具体操作是通过将对象的原点位置减去一半的x轴和y轴值,使其始终保持在屏幕中央。这样处理可以确保对象在屏幕上的位置稳定,避免偏移。然而,当前的渲染效果依然存在问题,呈现了许多离散的像素,这意味着需要进一步的优化和调整,以实现更精细的显示效果。在继续之前,计划再做一些调整和测试。
在这里插入图片描述

水平移动树并保持角度不变

为了实现平滑的动画效果,决定将对象沿x轴方向缓慢地左右移动。首先,固定x轴和y轴的位置,不做任何复杂的处理。然后,通过一个角度值来计算水平位移,使得对象在水平方向上以10像素为单位来回摆动。这样,物体的显示会呈现出缓慢左右摆动的效果,从而增强视觉效果。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

观察没有亚像素精度时的渲染效果

观察到的现象是值在不断地移动,物体的速度随着每一帧而变化,位置每次都会发生变化,但每次移动一个像素时,物体会在这个位置上“卡住”,即物体每移动一个实际像素就会发生一次位移。此外,物体的移动呈现出一种波动效果,如果加速移动的话,这种波动的效果会更加明显。实际情况是,通过逐像素的变化,像素值会从一个纹理单元(texel)变到下一个,形成了类似波纹的效果,这种波动和纹理的转换会导致可见的线条变化。
在这里插入图片描述

黑板:像素和纹素的数量没有均匀划分

在这个过程中,出现了一个问题,即像素和纹理单元(texels)之间的划分并不均匀。当进行放大时,一个屏幕上的像素可能会映射到多个纹理单元上。每个纹理单元的颜色值会被分配给对应的像素,但当像素和纹理单元无法完全均匀地匹配时,就会出现不平衡的情况。这意味着在纹理映射过程中,某些纹理单元可能会跨越多个像素,导致最终呈现出的颜色值并不完全与预期相符。

具体来说,纹理单元的宽度可能并不能完全覆盖每个像素中心,导致在某些位置,多个纹理单元对应同一个像素,进而造成颜色的重复或遗漏。当这种不均匀映射发生时,结果就是图像中的某些区域颜色值的分布不稳定,可能会产生一种视觉上的不协调感。

此外,由于纹理单元与像素之间的关系不精确,渲染过程中会出现“卡顿”现象,即物体的运动只会在完全达到下一个像素时才进行更新。这种不连续的更新会使得图像看起来不平滑,给人一种“跳动”或“卡顿”的感觉,这在视频游戏中尤为常见,尤其是在背景元素的渲染中,如云层等。由于没有使用足够的像素精度,背景的更新速度远低于其他元素的更新速度,造成了明显的滞后,影响了整体的流畅感。

这种现象经常出现在一些视频游戏中,尤其是当背景的运动和前景的运动没有同步时,容易让人感觉不自然,影响玩家的体验。

开始解决这些问题

现在的问题就是,像素与纹理单元之间的不均匀映射需要被解决。首先需要检查当前在像素空间中的位置是否准确,并确保在处理渲染时,能够精确地跟踪当前所在的位置。接下来,可以着手解决这些问题,逐步改进渲染的精度和流畅度。

注意到 DrawRectangleSlowly 忽略了传入的浮点精度

问题出现在代码中,特别是对原点的浮点精度的处理。当循环遍历像素时,忽略了浮点位置,直接使用了四舍五入后的像素值,这导致了位置的不准确。虽然这样处理并不完全准确,但通过在后续步骤中从像素位置中减去实际的浮点原点,能够恢复出准确的坐标。这保证了当进行边缘测试时,能够正确地测试像素的位置,并确保以正确的精度进行填充。

为什么我们看起来还是那样呢?

问题出在虽然知道精确的浮动位置,能够准确地确定每个像素对应到纹理图中的具体位置,但在操作中却使用了四舍五入,导致丢失了分数精度。尽管我们了解像素在纹理空间中的精确位置,最终还是会将其四舍五入到最近的纹理单元(texel),从而丧失了精度。

黑板:更好地弄清楚我们在纹理映射中的位置

为了精确地处理纹理采样,目标是通过线性插值来平滑地取样纹理中的颜色,从而避免简单地使用一个像素的颜色。当前做法是直接使用像素的颜色,但更好的方法是利用线性插值,结合四个像素的颜色,以得到一个平滑的结果。具体来说,假设我们有四个像素a、b、c、d,目标是通过线性插值在这些像素之间得到合适的颜色值。

首先,利用已经学会的线性插值,先在a和b之间进行插值,在c和d之间也进行类似的插值。接下来,我们需要处理的是如何在a、b、c和d之间进行插值。通过将插值过程重复一次,先将a和b、c和d分别插值,生成两个新的点,然后再对这两个新点进行插值,从而得到最终的平滑结果。

为了实现这一过程,需要两个分数值:一个用于水平插值,另一个用于垂直插值,这些分数值的范围都在0到1之间,表示离各自纹理单元(texel)的距离。通过这些分数值,能够实现对纹理的精确采样。

线性混合(Lerp)颜色

在处理纹理采样时,通过不进行四舍五入处理u和v的值,能够精确地获取纹理的实际位置。这些u和v值作为浮动小数点值,其中包含了分数部分。一旦完成四舍五入操作,可以通过从舍入后的值中减去之前的小数部分,轻松地恢复出原本的分数值。

具体来说,可以通过将纹理单元(texel)进行取样,并在水平和垂直方向上计算插值来实现平滑效果。在实现时,可以从纹理的四个相邻区域取样,依次为:当前位置的纹理单元、右侧的纹理单元、下方的纹理单元和右下方的纹理单元。通过这样的方式,结合之前的计算结果,可以将四个取样值进行混合,从而获得平滑的纹理效果。

在实际的代码实现中,可以利用纹理指针,逐一加上对应的偏移量来取样纹理值。首先从当前纹理位置取样,再取相邻的右侧纹理、下方纹理以及右下方纹理。通过这种方式,可以非常直观和有效地计算出纹理颜色的平滑过渡。

在这里插入图片描述

假装我们的纹理比实际小

在处理纹理采样时,可能会出现越界的情况,即尝试从纹理图的边界之外取样。为了解决这个问题,可以暂时做一个简单的处理:将纹理的宽度假设为实际宽度的两倍减去2,并在起始位置添加一个像素的偏移量。这样,实际上就是在一个比实际纹理小的矩形区域内进行取样,避免越界问题。

通过这种方式,可以确保纹理的取样不会越界,同时保持其他部分的效果不变。尽管这种方法并非最终解决方案,但作为一种临时的处理方法,它仍然能保证正确的纹理采样效果。在实际的实现过程中,通过这种调整,取样操作仍能按照预期工作,避免出现因越界而导致的错误或不一致现象。

在这里插入图片描述

将这些颜色混合在一起,但在此之前要拉升颜色值

现在的目标是将四个纹理样本进行线性插值,获得最终的纹理颜色。为了实现这一点,首先需要从每个纹理中提取出红、绿、蓝和透明度(Alpha)值。通过从每个纹理点获取这些值,接下来就可以进行插值操作。

在处理过程中,可能需要做一些必要的预处理,比如进行预乘透明度的操作,虽然这部分操作暂时没有做完,但也可以暂时跳过。接下来,完成插值时,需要根据每个纹理的坐标,将它们的颜色值结合起来。这个过程是通过在X和Y方向上分别进行插值来实现的。

通过这种方法,将两个纹理的颜色值插值得到一个新的中间值,然后再将两个插值结果进行进一步的插值操作,最终得到一个平滑过渡的颜色值。这种方式就是经典的纹理过滤技术,它使得纹理的过渡更加平滑,避免了因像素不匹配或边缘过渡不自然导致的视觉问题。

简而言之,完成了从纹理中提取颜色值并进行线性插值的步骤,最后通过合并插值结果实现了纹理的平滑过滤,确保了图像的质量和准确度。

在这里插入图片描述

制造一个 v4 用于在两个 Lerps 之间进行 Lerp 以及进行标量乘法的值

当前的问题是,缺少针对四个操作(如加法、减法等)的版本,特别是针对纹理操作的版本。为了解决这个问题,可以考虑制造适合的操作函数。事实上,插值(lerp)是可以通用的,无论进行什么样的操作,因此可以将其应用到不同类型的操作上。

如果使用模板编程,这个地方可以通过模板来实现,这样会更加简洁。然而,模板虽然非常灵活,但由于调试和使用上的困难,有时需要避免使用它。尽管如此,如果使用类似于JAI(Java AWT Image)的库,那么就能更轻松地处理这类问题,这种库提供了更高效的处理方式。不过,目前还没有使用这种库,因此必须继续使用C++来解决问题。

总体来说,解决方案的核心是实现合适的查找表和访问这些函数,确保能够顺利执行需要的纹理操作。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在游戏中查看我们正确的模糊渲染效果

现在,问题解决了,纹理已经变得适当模糊了,尽管有时候看起来好像停止了移动,但实际上它仍然在动,只是因为子像素的平滑度太高,肉眼无法看到波动的轨迹。这个现象其实是因为采样的处理已经让运动变得如此平滑,甚至无法察觉到它的移动,完全流畅地融入了视线。

通过改变纹理的采样方式,移动变得极其平滑,甚至连原本明显的跳动感都消失了。虽然目前的效果已经相当好,但可能仍然存在可以优化的空间,特别是在一些细节处理上。为了让平滑度更加明显,进行了一些调整,虽然在调整后依旧能看到一些轻微的卡顿,可能是帧率限制的问题。

为了进一步优化,尝试了缩小物体的大小,以适应当前不完全优化的帧率。这些调整使得物体的运动变得更加流畅,同时也能清晰看到纹理的细节和边缘的平滑度,虽然仍然存在一些细微的瑕疵,但整体效果比以前流畅得多。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

切换到旧方式和新方式进行比较

为了比较,回到使用旧方式的效果,进行了切换,观察了物体的移动。可以看到,在旧方式下,物体在高速移动时会有一些闪烁,这种现象是不可接受的,而且当物体到达终点时,会出现明显的停顿,表现出一种“僵硬”的运动。与之相比,使用平滑版本时,物体的移动没有闪烁感,停下来时也平滑地滑行,没有那种像素级的停顿感。

尽管由于帧率的问题,无法完全展现更大物体的平滑效果,但可以明显看到平滑版的移动远比旧版流畅。当切换回像素化版本时,可以清楚看到物体在移动时出现的“爬行”现象和停顿,表现得非常粗糙。而视频压缩可能掩盖了一部分细节,使得差异不那么明显,但仍能观察到明显的差距。

当前的渲染效果虽然还有很多需要改进的地方,但已经接近完成了基础的渲染需求,即填充纹理的四边形。接下来需要进行一些优化,尤其是在帧率上,还需加强以提升整体效果。此外,也需要落实一些细节,比如纹理的最终处理和进一步的纹理规范化。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

树旋转重新启动后

决定开始使用队列进行处理,同时让树重新旋转以便观察效果。旋转后的树展示了平滑版本和不平滑版本的差异。可以看到,在旋转时,未经平滑处理的版本显示出许多不自然的瑕疵和粗糙感,而经过线性过滤处理的版本则显得更加平滑,明显去除了很多伪影。

这种平滑效果就是通过线性纹理过滤实现的,这就是纹理过滤(Texture Filtering)的概念。当前使用的是最基础的双线性过滤器(bilinear filter),它通过在纹理的像素边界之间插值来产生平滑的效果,避免了硬边缘的结果。此外,除了双线性过滤外,还有各类高级纹理过滤方式,如各向异性过滤(Anisotropic Filtering)和各向空间过滤(Milkman Space Filtering),它们可以进一步提升效果,处理不同角度和距离下的纹理显示。

当前采用的基本过滤方法虽然简单,但能够有效去除纹理的硬边界,使得图像看起来更加平滑和自然。

在这里插入图片描述

树墙现在会有亚像素精度吗?

目前,虽然墙上的树已经像素准确地渲染了,但这些图形并没有按照预期显示。原因在于,实际上并没有调用绘制图形的例程。这些图形仍然通过旧的位图路径进行绘制,而不是通过新的、更快速、更准确的绘制方式。因此,直到新绘制路径被调用并正常工作,当前的图形依然会使用旧的绘制方式,导致它们未能显示出理想的效果。这也解释了为何树的渲染效果在此时并不完全符合预期。

树的顶部似乎被截短了

树顶被截短的原因有两个。第一个是因为在绘制时,实际上我们将树的顶部和两侧做了裁剪,只使用了每个边缘的一个像素。第二个原因是,由于现在使用了双线性过滤,我们不应该再加上0.5,而应该使用向下取整。这样可以避免四舍五入,确保正确获取两边的纹理。

通过去掉0.5,树顶会恢复一些,但仍然没有完全恢复。原因是,可能还存在一个问题,即裁剪了树的边缘。如果我们允许绘制区域进一步延伸,可能会导致从纹理外部取样,尽管这样做可以显示更多的树顶部分,但也会导致偶尔从纹理外部取样。因此,如何处理纹理边缘是一个需要进一步优化的地方。
在这里插入图片描述

在这里插入图片描述

树旋转会包含在 DLC 中吗?

这个功能可能会作为DLC发布,可能会是第一天的DLC。显然,玩家不应该直接免费获得旋转的树木效果,他们应该为此额外付费。但同时,也希望一开始就能在游戏中看到这个功能,可能会考虑让它作为零DLC提供。

它会有亚像素精度旋转吗?

现在旋转已经实现了亚像素精度,尽管还没有完全分析和处理所有细节,不能说完全没有问题。现在可以看到旋转的效果非常平滑,特别是当停止左右移动时,效果尤为明显。没有明显的伪影,且旋转时像素过渡非常柔和。不过,还需要进一步优化,特别是处理纹理边界问题,以避免树木被切掉的问题,这些都属于后续的精细调整和完善工作。

渲染器剩下的最重要/基础的任务是什么?

当前渲染系统最重要的任务是优化性能。虽然现在已经有了平滑的旋转效果,但如果要在整个屏幕上使用这种技术并保持高效,就需要解决性能问题,避免过度绘制等问题。虽然不一定需要立即达到非常高的速度,因为可以支持硬件路径,但另一个主要任务是实现OpenGL版本的渲染。何时进行这项工作取决于当前软件渲染的表现,如果软件渲染足够好,可能会继续依赖它。

SSE 限制

还需要考虑如何处理裁剪问题。对于这些对象,可能需要执行裁剪操作,但需要进一步思考具体的实现方式。

问题:如何为像素艺术图形实现平滑的亚像素渲染,而不模糊,但仍然能够平滑移动?

要平滑像素渲染,同时避免模糊效果,但仍然让图形平滑移动,方法会有些复杂。首先,需要根据具体的使用情况来决定实现方式。

黑板:为像素艺术图形实现平滑的亚像素渲染

要实现像素艺术图形的平滑渲染,同时避免模糊而保持图形的硬边界,可以采用以下几种方法:

  1. 像素着色器解决方案:理想的做法是在像素着色器中显式地处理。如果两个相邻的像素属于不同的纹理,才进行混合,否则保持不变。这样可以避免在图形平滑移动时产生不希望出现的模糊效果,同时保留硬边界。

  2. 多重采样(MSAA):启用多重采样并关闭线性过滤,可以让分散的像素拾取到两个纹理之间的正确边界。这种方法是一个较为便捷的解决方案。

  3. 手动预采样:这是最常见且性能较好的解决方案,尤其是在没有控制的引擎中。可以通过手动将小纹理进行上采样,将4x4的像素纹理扩大为更大的纹理(例如512x512)。通过这种方法,通常在大多数情况下每个像素都会显示为相同的颜色,只有在像素边界附近才会显示另一种颜色。虽然这种方法会占用大量纹理内存,但现代硬件的纹理内存已不再是问题,特别是对于像素艺术游戏而言,因为这些游戏的艺术资源通常并不大。

这些方法各有优缺点,选择合适的方法取决于项目的需求和硬件条件。

你能不能使用四舍五入+0.5并截断来避免采样到纹理的边界之外?

问题的关键在于如何确保像素采样不会超出纹理边界。为了避免这种情况,可以采用舍入和截断操作。具体来说,当进行像素采样时,通常会执行一个截断操作,然后加一,以确保每个采样都在纹理的有效区域内。为了确保准确性,可以避免加一,而是确保坐标始终距离纹理边界一像素,从而避免采样超出纹理范围。

这种方法看起来合理,但需要进一步分析和验证。虽然初步结果似乎表现不错,但还需要更多的时间进行细致的测试和调整,以确保这一方案能够完美地解决问题。
在这里插入图片描述

为什么你还要将 0.5f 加到 tX 和 tY 上?

目前仍然在对 txty 添加 0.5,这个问题在之前也被提到过,做出调整之前的问题已经得到回应。

如果仅测试 (u,v) 的域,而不是测试边缘,这样做会更快吗?

在性能优化方面,最终的做法可能会是基于中心坐标来进行测试,而不是测试边缘。这种方法可以避免过多地处理 uv,从而简化测试过程。这样做的一个潜在优势是,中心坐标可以直接用于查找,同时进行相同的坐标测试,可能会提高效率。然而,这个方法仍然需要进一步验证和优化,因为这并不是常见的编程实践,因此目前不能给出确切的答案。

CPU 上的渲染与 GPU 上的渲染在代码上有区别吗?

在 CPU 和 GPU 上渲染的代码差别不大,主要的区别在于循环的实现。在 GPU 版本中,渲染的循环会在 GPU 上进行处理,内部的具体操作与 CPU 上的处理类似。区别在于,GPU 会利用内建的硬件功能来进行某些优化,像是自动计算纹理坐标的查找等,而不需要手动计算。此外,GPU 版本可能还会通过宏和其他优化手段来加速执行。总的来说,GPU 在底层会执行更高效的代码,自动处理更多细节,从而实现更高效的渲染,但整体操作流程与 CPU 上执行的逻辑保持一致。

在像这样的软件渲染器中,使用四边形作为基本图元相比三角形是否有性能优势?

在硬件中,使用四边形作为基本图元比三角形更有性能优势。虽然三角形是3D图形中最基本的图元,因为它能够构建所有表面,并且三角形的平面方程是唯一能够定义平面的方式,因此硬件通常使用三角形。但是在精灵引擎中,四边形作为图元更加适用,因为精灵图形中不会涉及三角形的问题,而且使用四边形能够减少处理的复杂性。

对于精灵引擎,无论是在软件还是硬件中,四边形的处理通常比三角形更高效,因为它可以减少数据传输的量,提升吞吐量。简而言之,四边形能更简单直接地表达图形,因此在这种场景下,硬件处理四边形的速度会比处理三角形更快。

为什么树的边缘看起来有羽化效果?

树的边缘看起来像是羽化的,是因为它们实际上就是这样制作的。如果查看实际的艺术素材,就会发现这些边缘确实是羽化的。通过查看素材,可以看到它们是如何制作的,具体表现为边缘部分有逐渐变透明的效果,这就是为什么树的边缘看起来有羽化效果。

不同的渲染器会在平台层中吗?

不同的渲染与平台层并没有直接关系,因为它们大多数时候是平台独立的。因此,渲染会有所不同,通常会在适当的时候将其提取到一个第三层次中,这个三层次结构是之前多次提到的。

我看到有个叫 ‘Computer Color is Broken’ 的视频,它讲述了颜色混合通常是如何错误处理的。如果不依赖第三方库,你有机会做得更对。

在视频中提到的色彩混合通常没有依赖第三方库来正确实现,视频讨论了如何正确地进行色彩混合,尤其是在高动态范围(HDR)和物理基础光照模型中,这些情况通常需要考虑到色彩的伽马校正。然而,在处理像素精灵图时,通常不需要考虑伽马校正,虽然正确的边缘混合可能会有所改善。对于精灵引擎来说,通常不会推荐进行伽马校正,除非有非常充分的理由。

然而,由于某些原因,需要承诺展示如何进行正确的伽马校正,因此会进行伽马校正。校正方法通常涉及平方和平方根的操作,并使用色彩查找表进行处理。虽然结果可能并不十分显著,但至少能略微改善效果。

记得上周介绍了 RenderGroup 吗?这个亚像素渲染的 lerp 是不是会放入 RenderGroup 中?

在上周介绍的渲染组中,DrawRectangle已经被包含在渲染组内,并且是从渲染组内部的RenderGroup中调用的。这里使用的坐标系统是一个简单的调试工具,目的是确保渲染能够正确工作。在确保渲染过程能够正常工作之后,目标是让其他部分的渲染都通过一个统一的调用路径进行,从而使渲染过程更为规范和高效。

今天就到此为止,回顾并展望未来

今天完成了线性混合纹理映射,进展顺利。原本以为渲染部分会需要两个月的时间,但实际上只花了一周就实现了大部分功能。接下来会继续优化数学部分,并在之后开始优化工作。尽管渲染部分的工作看起来没有预期的复杂,目标还是尽早开始制作游戏,享受游戏开发的乐趣。接下来计划进一步完善数学模型,讨论像素中心、填充规则、色彩等内容,并确保渲染过程更加精简和高效,减少像素测试和操作次数。


http://www.ppmy.cn/devtools/157961.html

相关文章

vue学习6

1. 智慧商城 1. 路由设计配置 单个页面&#xff0c;独立展示的&#xff0c;是一级路由 2.二级路由配置 规则&组件配置导航链接配置路由出口 <template><div id"app"><!--二级路由出口--><router-view></router-view><van-…

Transformer

1 认识Transformer 定义:Transformer是基于Seq2Seq架构的模型,可以完成NLP领域研究的典型任务,如机器翻译,文本生成等,同时又可以构建预训练语言模型,用于不同任务的迁移学习。 //本篇主要介绍通过transformer架构实现从一种语言文本到另一种语言文本的翻译工作。使用NL…

vite + axios 代理不起作用 404 无效

vite axios 代理不起作用 先看官方示例 export default defineConfig({server: {proxy: {// 字符串简写写法/foo: http://localhost:4567,// 选项写法/api: {target: http://jsonplaceholder.typicode.com,changeOrigin: true,rewrite: (path) > path.replace(/^\/api/, )…

c++ haru生成pdf输出饼图

#define PI 3.14159265358979323846 // 绘制饼图的函数 void draw_pie_chart(HPDF_Doc pdf, HPDF_Page page, float *data, int data_count, float x, float y, float radius) { float total 0; int i; // 计算数据总和 for (i 0; i < data_count; i) { tot…

小红书爬虫: 获取所需数据

小红书&#xff0c;又名 “小红书 ”或简称 “红”&#xff0c;已迅速成为中国社交和电子商务领域的重要参与者&#xff0c;成为一个不可或缺的平台。对于企业、营销人员和数据分析师来说&#xff0c;从小红书收集数据可以获得宝贵的洞察力&#xff0c;从而推动业务增长。虽然这…

stm32电机驱动模块

电机驱动模块是智能车等电子设备中用于驱动电机运转的重要部件&#xff0c;它能将微控制器输出的控制信号转换为足够的功率和电流来驱动电机。以下为你详细介绍电机驱动模块的相关信息&#xff1a; 常见类型 1. L298N 电机驱动模块 特点 高电压、大电流驱动能力&#xff1a;能…

基于ESP32的远程开关灯控制(ESP32+舵机+Android+物联网云平台)

目录 材料环境准备物理材料软件环境 物联网平台配置&#xff08;MQTT&#xff09;MQTT阿里云平台配置创建产品添加设备自定义topic esp32配置接线代码 Android部分和云平台数据流转 前言&#xff1a;出租屋、宿舍网上关灯问题&#xff0c;计划弄一个智能开关以及带一点安防能力…

Git stash 暂存你的更改(隐藏存储)

一、Git Stash 概述 在开发的时候经常会遇到切换分支时需要你存储当前的更改&#xff0c;如果你暂时不想应用当前更改也不想放弃更改&#xff0c;那么你可以使用 git stash先将其隐藏存储&#xff0c;这样代码就会变成未修改的状态&#xff0c;等解决其他问题后&#xff0c;在…