欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 产业 > 游戏引擎学习第95天

游戏引擎学习第95天

2025/2/11 16:02:34 来源:https://blog.csdn.net/TM1695648164/article/details/145546531  浏览:    关键词:游戏引擎学习第95天

回顾昨天的内容

我们一起完成游戏开发。我们正在进行自定义渲染的工作,这非常棒。我们基本上是在实现一个GPU的功能,自己来做这一切,这样我们可以看到它是如何运作的。

令人惊讶的是,整个过程并没有花费太多时间。当最初想到要实现这个渲染器时,心里想着“哦,这将是一个大工程”,毕竟很久没有做过软件光栅化了。想象中这会是一个很大的任务,但实际上,我们在几天内就完成了大部分的基础工作,这让人感到有点奇怪,但这也正是事实。我希望接下来的过程也能这样顺利进行。

我们上次做了一个关于伽玛校正的简短插曲,这是应网友的要求进行的。具体来说,我们为我们的纹理映射例程添加了伽玛校正,这样我们就可以绘制旋转和缩放过的精灵了。我们把这部分做了一个基本的完成,但有些人还是在提问,尤其是在论坛里,有人问了伽玛如何与预乘alpha相互作用。

回顾我们之前做的事情,伽玛校正的步骤是在RGB到线性空间的转换中进行的,确保了所有的数学计算都在RGB 255到255的范围内进行。这一过程确保了我们处理纹理的每一步都符合预期。

什么时候需要预乘alpha?

有一点我们没有讨论到,但这是一个值得提及的内容,那就是在进行预乘alpha时,什么时候准确地预乘alpha是一个问题。因为在做RGB到线性空间的转换时,看起来我们应该在这个过程中预乘alpha。也就是说,我们希望在进行alpha混合时,确保在进行这个操作之前已经将颜色值转换到线性空间,这样我们就能在一个正确的空间进行alpha的预乘,然后再进行后续的计算。

实际上,我们没有仔细检查过,但我相信我们当前的处理方式基本是正确的。虽然我并不是权威专家,但我认为我们已经尽可能地做到正确了。如果能有专业的渲染专家来验证这一点会更好。

接下来,当我们对现有处理方式进行优化时,可能会遇到一个常见的问题:如何存储颜色值。具体来说,如何将颜色值存储为已经预乘alpha的形式。也许我们不希望每次都在这里进行alpha的预乘,可能希望在其他地方处理这个问题。这个问题涉及到如何将颜色值转换和存储,所以我们最好在需要时再处理这些细节。

目前,我们的做法是:在从纹理中采样时,我们得到的并不是预乘alpha的颜色值,而是原始的sRGB值以及用在图像编辑软件中的alpha值。然后,我们将这些值转换为线性空间的值,并在此基础上进行非预乘alpha的混合,最后在合成时进行alpha合成处理。

这个双线性混合可能需要使用预乘alpha

现在想到这个问题,实际上在进行双线性混合时,可能需要使用预乘alpha,因为在进行混合时,alpha应该已经预乘到颜色值中。因此,如果我们想让这个过程更正确,我推测我们需要在gamma校正之后,对这些值进行预乘alpha的处理。

具体来说,在加载纹理时,我们应该将alpha提取出来,并在处理时将其与颜色值一起预乘。这意味着在进行颜色计算时,需要确保我们正确地将alpha值与其他颜色通道(如红色、绿色、蓝色)进行乘法运算,而不是仅仅将其与颜色分量单独存储。

在仔细检查代码后,似乎我们在之前的处理步骤中漏掉了alpha的乘法操作。我没有看到在代码中哪里做了alpha的乘法,可能是在某个步骤中不小心去掉了。因此,我觉得我们应该在计算中重新引入这个alpha乘法的步骤,以确保预乘alpha的正确性。

检查树木外观的变化

我们检查了预乘alpha的效果,但看起来变化几乎不可见。为了方便测试和调试,我们解决了一个问题,使得渲染过程不再显得过于奇异(即不再出现三色光的迷幻效果)。这种效果可能会干扰我们专注于实际渲染的测试。因此,我们调整了这个视觉效果,让它变得更加平稳,以便更好地集中精力。

当我们执行alpha预乘时,理论上应该看到效果,尤其是在图像的边缘区域。我们尝试将颜色乘以alpha,但实际上几乎看不到明显的变化。这个结果表明,很多渲染和图形处理上的正确性问题,实际上很难在实际图像中察觉到,尤其是在像这种流媒体显示的环境下,很多细节会被模糊掉。即使我们观察到微小的变化,也不容易在屏幕上清晰地看到。
在这里插入图片描述

在这里插入图片描述

检查我们是否存储了预乘的Texels(我们存储了)

在检查了我们的纹理之后,发现似乎我们的纹理没有正确地进行预乘alpha,或者是因为我们没有在存储时就进行预乘alpha。我最初的理解是这样,但随后我们回顾了加载位图的代码,发现我们实际上已经在加载过程中将它们存储为预乘alpha。所以,问题的原因是我们在存储时已经预先进行了alpha预乘处理,因此在后续的计算中并不需要再次处理alpha值。
在这里插入图片描述

我们并没有做最正确的事情

经过思考,发现我们并没有做最正确的处理。具体来说,问题在于我们将纹理从sRGB转换为线性空间时,纹理已经在sRGB空间中进行过了alpha预乘。因此,在处理边缘部分时,尤其是那些有部分透明度的地方,我们得到的sRGB值并不完全正确,因为这些值已经在sRGB空间中经过了预乘处理,这并不是我们想要的结果。

不要预乘alpha

为了更正确地处理这个问题,一种方法是在加载位图时不对alpha值进行预乘。这样,我们就不会像之前那样直接在加载时进行预乘。这样做的结果是,屏幕上其他被混合的部分会出现错误,尤其是树木部分,它会被正确地处理,而其他部分会变得不正确。通过这种方式,加载的位图可以正确地避免alpha预乘。
在这里插入图片描述

在混合之前才将alpha预乘

如果位图没有进行预乘,那么正确的做法是,在将它们转换到线性空间时,进行alpha值的预乘。这样,rgb值将会乘以alpha值,确保所有的值从未进行预乘转换为已预乘的alpha。这样,混合操作将在正确的预乘空间中进行,从而确保混合过程正常。之后,可以按照预期进行其他操作。
在这里插入图片描述

输出时我们的Color值不应预乘

目前的操作中,输出时的alpha值应当是不经过预乘的。这样在输出颜色值时,需要对alpha进行适当处理。虽然这个更改可能不会显著地影响可视效果,但这样做是更加正确的做法。首先将sRGB转换为线性空间,再进行alpha预乘,接着进行混合处理,按这种方式操作是正确的。

重复混合会发生什么?

在反复进行混合时,尤其是在处理多个图层叠加的情况下(比如生成地面块时),需要考虑可能出现的问题。如果最终的图层(如草地)具有透明部分,而我们以预乘alpha的方式输出颜色值,那么RGB值实际上会被alpha预乘,这并非我们期望的结果。需要认真考虑这个问题,并理解它可能带来的影响。虽然通常情况下我们希望所有内容都已预乘alpha,这样能简化存储和操作,但实际情况较为复杂。通常情况下,硬件上会直接存储预乘后的数据,而不是反复计算。这是一个较为复杂的情况,需要深入思考正确的处理方式。

Blackboard:当我们的值没有预乘时

在讨论如何处理颜色和透明度时,首先需要将颜色从sRGB空间转换为线性空间。转换后,颜色可以进行各种操作,但透明度的处理需要特别注意。透明度(alpha)值如果没有预乘,则在进行计算时会出现问题,因此必须在计算之前将其预乘。

为了正确处理透明度,颜色值和透明度值需要乘以透明度。最终得到的结果将是一个已经预乘透明度的颜色值,这个值可以存储在缓冲区中。如果稍后需要将其恢复到原始状态,则必须用透明度值的倒数进行反向乘法,但这存在潜在问题,因为如果透明度为零,倒数将会导致除以零的错误。因此,为了避免这个问题,最佳做法是直接存储已经预乘透明度的值。

Blackboard:投影回sRGB

可以考虑一种潜在的解决方案,就是先将颜色值转换为线性值,然后再乘以透明度(alpha),最后再转换回sRGB空间。这种方法可以确保存储的颜色值是正确的,尤其是在取出时仍然保持线性值。这意味着在处理颜色时,应该将线性值与透明度相乘,然后将结果投影回sRGB空间,而不是将透明度预乘到sRGB空间后再转换。

然而,这种方法并不简单,因为视觉上的差异往往不容易察觉,所以很难仅凭图像判断是否有效。为了进一步验证这一点,可以通过数学方式进行测试,或者编写一些小测试来绘制和比较期望的结果。尽管如此,最终是否需要这样做,还需要更深入的分析和测试才能做出决定。

在存储之前将Color值投影回sRGB空间

可以考虑将透明度值通过线性空间处理,但不直接将其乘以透明度,而是先将sRGB值转换为线性空间,然后对RGB值进行透明度预乘操作,最后再将其转换回sRGB空间。在这种方法中,首先读取纹理中的RGB和alpha值,然后将其转换为线性空间,再乘以alpha值完成预乘,最后将结果投影回sRGB空间并存储。这样,存储的值将保持正确的预乘透明度,并且可以确保在提取时能够恢复正确的线性值。

这一方案的核心思想是避免直接在sRGB空间进行透明度的预乘,而是通过先转换为线性空间,处理透明度后再转换回sRGB,从而确保在存储和读取过程中透明度的处理保持一致。
在这里插入图片描述

添加包含rgb和a的结构体

遇到问题时,可以通过修改数据结构来解决。在这种情况下,使用一个联合体(union),将RGB值和透明度值整合在一起,可能会有一些冲突,因此需要谨慎处理。通过这种方式,可以将RGB和alpha信息一起存储,尽管这种做法可能不太完美,但可以有效地解决问题。接下来,加载数据时,应该确保验证这些数据的正确性,以避免潜在的问题。
在这里插入图片描述

在这里插入图片描述

在游戏中检查一下,并考虑让所有例程都支持gamma

在加载数据时,发现原本希望的效果并没有出现。问题出在,对于没有进行伽玛校正的例程,当前的处理方式可能导致不正确的混合效果,因为这些例程没有在处理后进行转换。为了解决这个问题,可以通过修改其他例程,使它们能够识别并处理这种情况。具体来说,可以在加载sRGB值时,进行转换操作,这样就能确保无论哪条路径都能保持一致。当然,另一种选择是暂时忽略这个问题,直到所有操作都通过统一的绘制位图路径进行处理,这也可以作为一个短期解决方案。

转换DrawBitMap以正确处理gamma

在处理纹理时,首先需要加载纹理并进行处理,就像之前一样。具体来说,在加载时需要进行sRGB到线性空间的转换,并且在每个纹理的RGB值上进行α值的预乘。在此过程中,先将纹理值转换为线性空间,然后将其与α值相乘,计算出一个中间值。

接下来,读取目标值并进行相应的处理。目标的RGB和α值被提取出来,然后进行类似的操作。最终将结果存储回去,并确保在存储前进行适当的sRGB到线性空间的转换。

在代码实现过程中,特别注意确保类型和变量名称正确,例如目标值的α值(d)需要正确声明。在处理纹理时,可以将每个通道的值进行线性计算,然后通过合适的操作将它们恢复到目标空间。在此基础上,进一步简化代码并消除冗余操作,以保持代码的整洁。

最终目标是确保所有路径的处理一致,避免在处理中出现不必要的错误或不一致。
在这里插入图片描述

在这里插入图片描述

尝试并在游戏中查看效果

在调试过程中,发现渲染过程中出现了一个空白屏幕的情况,这通常会让人困惑。经过一些调整后,发现问题是由于当前的路径处理非常慢,导致所有地面块的绘制变得非常耗时。虽然现在的背景合成看起来正常,树木也显示如预期,但由于处理速度极慢,整体渲染变得非常缓慢,影响了性能。尽管如此,问题的根本原因已经找到了,并可以继续优化性能,逐步改进渲染效率。

考虑让我们的软件渲染器运行得相对快速

在调试过程中,发现启用优化后,性能有了显著提升。编译时启用优化与未启用优化之间的差异非常大,尤其是在处理屏幕上所有像素的情况下,优化模式能显著提高渲染速度,而在调试模式下则显得异常缓慢。因此,需要在构建过程中做出一些调整,以确保渲染性能足够好。

为了处理这一问题,可以考虑将构建分成两个文件:一个专门用于渲染像素的例程,其中启用优化,另一个用于其他部分,保持调试模式。这样可以在不影响其他调试过程的情况下优化渲染性能。

此外,在处理预乘 alpha 时,确保所有的计算都在线性空间中进行,之后再转换回 RGB 空间。这样可以保持颜色的一致性并避免错误,同时也减少不必要的计算,进一步提高效率。

在这里插入图片描述

再次查看DrawRectangleSlowly并提前预乘Color

这个讨论涉及了颜色预乘和混合的优化。目标是通过预先将颜色与透明度(alpha)进行乘法运算,从而减少每次混合时的计算量。具体来说,首先通过将颜色与透明度预先相乘(即“预乘颜色”),然后在后续的操作中就只需要与 alpha 值进行乘法计算,而不需要再次计算颜色的乘法。这样做能够避免每次都进行重复的乘法,从而提升效率。

具体的步骤是:

  1. 在预乘阶段,先将颜色与 alpha 值相乘,这样之后每次计算就只需用到已经预乘好的值,而不需要在每次混合时重新计算 alpha 和颜色的乘积。
  2. 接着,将颜色进行乘法操作,与调节值(如 RGB)进行相乘,调整透明度(alpha)来改变整体的透明度。
  3. 然后从目标像素中读取数据,假设这些数据已经按照预定的方式存储(例如在 alpha 和颜色通道上有正确的处理)。
  4. 计算反向 alpha 值,考虑到已经预乘过的 alpha 和颜色的 alpha 相乘。
  5. 最后,在线性空间中进行混合操作,并且把混合后的结果从线性空间转换回帧缓冲。

这个过程的目标是确保渲染计算的效率,同时保证混合的正确性。由于在渲染过程中很难一眼就发现小的错误,因此需要特别关注转换过程中是否存在任何不准确的地方。为了进一步验证,可以考虑制定一些测试用例,来检查是否存在计算错误,尤其是在颜色转换和 alpha 值处理的过程中。

在这里插入图片描述

修改之前commit

方法:使用交互式 rebase 修改历史中任意 commit 的 message

  1. 启动交互式 rebase:
    如果你想修改历史中某个 commit 的 message,可以使用交互式 rebase。从你要修改的 commit 之前的 commit 开始:

    git rebase -i <commit_id>^
    

    其中 <commit_id> 是你想修改 message 的 commit 的哈希值。^ 表示从该 commit 的前一个 commit 开始 rebase。

  2. 标记为修改 message:
    在打开的编辑器中,你会看到如下内容:

    pick <commit_id> <commit message>
    pick <commit_id> <commit message>
    pick <commit_id> <commit message>
    

    将你想修改 message 的 commit 旁的 pick 改为 reword

    reword <commit_id> <commit message>
    
  3. 修改 commit message:
    保存并退出编辑器,Git 会暂停在这个 commit,让你修改 commit message。修改完成后,保存并退出。

  4. 完成 rebase:
    修改完后,Git 会继续进行 rebase。执行:

    git rebase --continue
    
  5. 推送修改:
    如果修改的 commit 已经推送到远程仓库,并希望同步修改到远程仓库,执行强制推送:

    git push --
    git push origin HEAD:<remote-branch-name> -f
    

这样,你就可以修改历史中任何 commit 的 message。

在这里插入图片描述

做一点清理

在接下来的十五分钟里,计划做一些清理工作。决定不开始新的任务,而是对现有的内容进行整理。如果回顾之前的渲染组,已经去掉了头部(header)。去掉这个头部是一个不错的决定,因为之前每个要发送到渲染器的元素都必须有一个渲染组头部条目,这样做有点容易出错,而且实际上没有必要这么做。

可以通过简化流处理来避免使用头部。在推送元素时,只需要确保缓冲区在添加数据时会自动处理大小,直接增加头部的大小。这样在推送时,就能确保头部先被放入,然后再放入其余的内容。

这种做法的关键是当将渲染元素推送到缓冲区时,先计算出需要增加的空间大小,确保头部和其余内容都有合适的空间,然后就可以设置头部并指定其类型。这是一个简单直接的改进方式,避免了不必要的复杂性。
在这里插入图片描述

Blackboard:Header是如何工作的

之前的做法是将某个数据推送到缓冲区,数据的大小已知,并且第一个部分是一个头部。这个头部用于标识数据的结构,并且通过返回后,颜色部分会根据这个头部来进行设置,处理这个头部信息。

现在的思路是将这个过程分为两个顺序的推送操作。首先推送头部,然后再推送数据部分。这样,数据部分的处理方不再需要关心头部的存在。头部只作为一个内部控制的信息,用于流控制,帮助理解数据结构的布局。

对Header和Data进行两次顺序推送

在这个场景中,首先讨论了如何将数据推送到缓冲区。推送的内容包括所需的大小和头部信息。首先计算出数据和头部总大小,然后检查是否有足够的空间。如果有足够空间,就定位到头部的位置,设置其类型,并返回结果,这个结果就是头部位置加上1,也就是头部之后的位置。

接着,讨论了如何使用指针进行偏移处理。在C语言中,指针算术会自动帮助定位头部之后的位置。因此,头部的位置加1就是下一个字节的位置,即头部的大小加上偏移量。为了清晰起见,作者提供了一种替代写法,明确地显示了如何处理头部和数据指针的关系。

接下来提到需要调整现有的代码逻辑,避免直接使用头部作为类型转换。应该让代码根据数据指针来定位,确保指向头部之后的位置,这样就能正确地访问数据部分。具体做法是读取头部后,指针会自动指向数据部分,而数据部分和之前处理的方式是一样的。

最后,提到返回的结果应该是数据指针,而不是头部。需要注意返回的数据类型应该是数据指针,而非头部指针。
在这里插入图片描述

在这里插入图片描述

步进查看并确保它工作正常

在这个场景中,首先讨论了如何通过调试来检查代码的执行情况。提到通常会首先推送清除操作,接着分析了清除操作的过程。首先要推送的内容是一个包含16个字节的渲染条目结构,然后加上头部的大小(4字节)。这样就能计算出总的大小,并确保有足够的空间。

接着,获取到指向头部的指针,头部是缓冲区中的第一个部分。然后设置头部的类型,虽然头部的类型已经是零(所以没有变化)。接下来,设置结果指针,结果指针会位于头部后面4个字节的位置,这样就能正确地指向数据部分。

通过调试,验证了结果指针的偏移是否正确。确认指针偏移是预期的4个字节,并且通过计算result指针减去header指针,得到了正确的头部大小(4字节)。然后,所有内容都向前移动,完成了预期的操作。最后,认为这个过程应该是正常工作的。

在这里插入图片描述

在这里插入图片描述

正确递增BaseAddress并在游戏中检查

在这个过程中,首先提到的问题是指针没有正确地递增,特别是在处理基础地址时,忘记了在读取头部后更新基础地址。为了修复这个问题,需要在读取头部后,增加基础地址的偏移量,以考虑头部的大小。这样就能确保代码在使用时不需要关注这些细节,从而使代码更加简洁。

接着,提到了性能问题,特别是在位图绘制上变慢。虽然当前的操作可能导致速度变慢,但计划很快采取措施来加速这一部分。提到了一些优化方法,但也意识到在调试时,如果进行位置比较或者引入更多的优化,可能会使得调试变得更加复杂和困难,因为指令指针会跳动,这会增加调试的难度。

最后,回顾了当前的代码清理工作,确认没有更多的小规模改动需要做,也没有引入新的复杂功能。没有必要再加入新的内容,除非是开始处理那些需要更多工作的大规模任务。
在这里插入图片描述

网络:MSDN: ‘optimize pragma’

在 Visual Studio 中可以使用优化指令(pragma)来控制编译器的优化行为。通过这些指令,可以在代码中启用或禁用优化功能,例如通过设置优化开关为“on”来尝试启用全局优化。这些优化选项包括快速机器代码生成等。但问题在于,不确定这些指令是否能完全达到预期的优化效果,尤其是启用优化时,是否能够触发所有需要的优化。虽然可以通过设置“off”来关闭很多优化,但对“on”的具体效果没有完全的把握。

此外,还提到,当使用优化指令时,它实际上可能不会完全按照预期优化,因为它会重置为指定的编译选项,而这些选项可能并不包含所有所需的优化。尽管如此,还是希望能够通过这种方式实现一些优化,认为如果能有效工作将会是个不错的选择。
-optimize pragma
#pragma optimize 是 Visual C++ 编译器中的一个指令,允许开发者控制代码的优化级别。它可以用来启用或禁用特定的优化选项。

#pragma optimize 指令

语法
#pragma optimize( " [ 优化列表 ] ", { on | off } )
备注

#pragma optimize 指令用于指定逐函数的优化。

  • #pragma optimize 必须出现在函数外部。它会在指令后的第一个函数定义时生效。
  • onoff 参数分别用于开启或关闭在优化列表中指定的优化选项。

优化列表(optimization-list)可以包含零个或多个参数,这些参数在下表中有所列出。

优化列表参数
参数优化类型
g启用全局优化(已弃用)。更多信息请参见 /Og(全局优化)。
st指定简短或快速的机器码序列。
y在程序栈上生成帧指针。

这些参数与 /O 编译器选项使用的字母相同。例如,以下指令等效于 /Os 编译器选项:

#pragma optimize( "s", on )  // 启用空间优化
特殊形式

使用空字符串 "" 作为优化列表是一种特殊的指令形式:

  • 使用 off 参数时,会关闭所有优化(gsty)。
  • 使用 on 参数时,会将优化重置为使用 /O 编译器选项时指定的优化。

例如:

#pragma optimize( "", off )
/* 未优化的代码段 */
#pragma optimize( "", on )
相关信息
  • #pragma optimize 指令与 __pragma_Pragma 关键字一起使用。

可能的限制

使用 #pragma optimize 时要小心,因为它可能不会按照预期的方式优化代码,尤其是在涉及多个编译选项时。有时,优化功能会根据编译器的版本或配置发生不同的行为。因此,最好在编译设置中明确地配置优化选项,而不仅仅依赖于 #pragma optimize

尝试使用 #pragma optimize

尝试了使用 #pragma optimize 来优化特定的例程,目的是避免将代码拆分到不同的文件中。然而,尽管启用了全局优化设置,实际效果并没有预期中的好,尤其是在优化绘制位图的性能时。通过设置全局优化和启用快速机器代码优化,期望能够提升性能,但实际表现并没有显著改善,速度远不如之前。

尽管启用了优化指令,似乎没有带来预期的效果,因此怀疑这些指令并没有真正起作用。为了确认是否有效,考虑进一步检查 Visual C++ 的相关文档或其他资源,看是否有类似的情况或解决方案。然而,当前看来,这种优化方法并未显著提高绘制位图的性能。

在这里插入图片描述

网络:MSDN: ‘优化最佳实践’

进行了对优化的研究,目标是理解如何在编译过程中启用优化。尽管有一些关于禁用优化的讨论,但实际上需要做的是启用优化,以便更好地理解编译过程中的优化机制。尝试过将代码包裹在#pragma optimize off中,但这种做法并不理想,因为它会关闭所有优化,而实际上是希望启用优化。接下来可能需要在优化时进行进一步的尝试和调整。

尝试将其他代码包裹在 #pragma optimize off 中(不行)

尝试了在代码中使用 #pragma optimize 来启用或禁用优化,但遇到了一些困难。尝试过将代码块包裹在 #pragma optimize on#pragma optimize off 中,理论上希望能够控制优化的开启和关闭。然而,实际情况并没有得到预期的效果,代码依旧没有得到有效优化,可能是由于内联函数的影响,或者优化指令没有按预期工作。因此,考虑到优化和调试的需要,决定放弃此方法,并重新调整策略,可能需要在编译时先进行一些优化,以便能在关闭优化的情况下进行代码逐步调试。

这种方法虽然不完美,但对调试和教学有帮助,因为在启用优化后,代码会重新排序,调试时难以跟踪代码的执行过程。所以,为了能够在调试时直观地展示代码执行,决定采用更清晰的方式来处理优化。未来可能会将需要优化的部分提取到独立的文件中,进行更加模块化的优化和管理。

在这里插入图片描述

不行

在开启优化的情况下调试有用吗?你认为以后有机会演示如何做到这一点吗?

如果想要在优化开启的情况下进行调试,需要接受优化可能导致代码跳转的情况,因为这是优化的正常行为。为了让调试过程更顺利,可以使用 Visual Studio 的调试优化开关。通过这个开关,可以帮助在调试时减少优化带来的困扰。

演示 /Zo 编译器标志

在调试开启优化的代码时,优化可能导致代码跳转,使得调试过程变得更加复杂。例如,在优化模式下,变量的值可能无法直接看到,因为编译器通过优化重排了代码,使得变量的位置不再明显,调试器无法直接查询到变量值。在这种情况下,需要通过查看汇编代码来找到变量的位置,这既耗时又容易出错。

为了改善这种情况,可以在构建过程中使用 -Zeo 开关,开启更详细的调试信息。这将使生成的PDB文件包含更多的注解,帮助调试器更好地识别代码中的变量和源代码位置。尽管如此,这也无法解决优化所带来的代码重排问题。优化后的代码通常会因为更高效的执行顺序而跳转,这使得调试更加困难。

因此,建议尽量避免在优化模式下进行调试,而是应在调试模式下工作,只有在必须调试优化模式下的代码时,才启用优化。如果代码只有在优化模式下才会出现问题,启用 -Zeo 可能有助于获得更多调试信息,但调试仍然比在非优化模式下更加复杂。

GLSL

GLSL(OpenGL Shading Language)代码主要涉及着色计算,它并不直接处理像素填充的过程。GLSL只负责计算输入和输出,处理的是缓冲区数据,而不是像素本身。渲染过程中的大部分工作,特别是在2D游戏中,都是由光栅化部分完成的,即决定哪些像素需要填充。因此,GLSL本身在2D游戏中的作用相对较小,主要用于一些基本的着色计算,例如法线贴图查找和光照等。

对于2D游戏,渲染的核心部分是光栅化,它决定了哪些像素需要处理,而GLSL只是在计算颜色和光照等方面提供帮助。由于2D游戏的着色代码通常比较简单,所以GLSL代码在整个渲染流程中的作用并不显著。

在3D游戏中,情况则有所不同。3D渲染可能涉及更复杂的着色器,例如细分着色器、几何着色器、顶点着色器以及像素着色器。这些着色器可能更加复杂,因为涉及到更多的计算和效果,像是近似处理、后期处理等。在这种情况下,使用GLSL代码就显得更加有意义。

尽管如此,对于一个软件渲染的2D游戏而言,使用GLSL并不提供太多优势。GLSL本质上只是一种编程语言,虽然它可以帮助描述着色计算,但没有必要将其作为软件渲染的核心部分。更实际的做法可能是使用宏语言来扩展GLSL或直接编写自己的代码,而不是将GLSL作为源语言进行编译。

总结来说,GLSL在2D游戏的渲染中并不是一个关键部分,因为大多数渲染工作都在光栅化阶段完成,而着色计算只是其中的一小部分。如果游戏中需要大量的着色器代码,可能才需要考虑使用GLSL来统一描述这些计算,但对于大多数2D游戏来说,GLSL并不是必需的。

修改今天做的预乘alpha之后,LoadBitmap函数是否也需要优化?

在进行今天的更改后,加载位图的函数可能不需要特别优化,因为目前并没有从位图中加载数据,所以这个函数的执行时间可能非常短。优化的重点可能应该放在地面准备和屏幕混合这两个部分,因为它们更有可能占用大部分的时间。因此,加载位图的函数优化可能不会带来显著的性能提升。

这样的动态数组在内存系统中如何工作?它是跨帧持续的,但可以改变大小。目前内存区域中不支持有空洞吗?

在当前的内存系统中,内存领域不支持存在“空洞”,因此要实现一个动态数组,通常会依赖动态分配器。动态分配器的工作原理类似于mallocnew等函数,它们会分配一大块内存,然后按需划分。如果出现空洞,分配器会尝试用其他大小相似的内存块填补这些空洞。因此,如果需要完全动态的内存分配系统,就需要实现一个动态分配器,这并不困难。

然而,需要谨慎考虑是否真的需要一个动态数组。通常情况下,动态数组并不是最佳选择,因为它可能导致内存碎片化,哪怕使用了动态分配器。此外,动态数组可能还会带来其他问题,比如内存分配的不可预测性。因此,在考虑使用动态数组之前,建议仔细评估是否真的需要这种动态内存分配方式。

编译器难道不能将OpenGL命令翻译为C命令,并将它们包装在一个迭代像素的循环中吗?

的确,编译器可以将OpenGL命令转换为相应的命令,并将它们包装在一个循环中以便对像素进行计算。然而,实际操作中并不必要这么做。以绘制矩形为例,核心代码中包括了纹理映射矩形的部分,但其中的着色器部分非常简单,主要是处理多次相乘操作,这通常可以通过一个GLSL流水线来实现。

然而,代码中的其余部分,尤其是与纹理单元相关的部分,并不是GLSL的一部分。纹理的处理、像素的计算以及混合的部分,都不属于GLSL范围。因此,实际上在GLSL中只涉及到少量的代码,主要集中在着色器的计算部分。

从效率角度来看,将整个渲染过程转换成GLSL并不值得,因为所需的GLSL代码量非常小,无法带来足够的性能提升。引入复杂的GLSL语言和转换过程只会增加不必要的复杂性,尤其在只有少量着色器代码时。这种做法更适合在渲染代码量非常庞大时使用,但在大多数情况下,这样的转换并不具有实际意义。

你能解释一下在关闭游戏时是如何释放所有内存堆栈的吗?

在关闭游戏时,现代操作系统(如Windows)会自动丢弃所有的内存,因此不需要显式地释放内存。当关闭程序时,操作系统会清除程序占用的所有内存空间,不需要开发者手动去管理或释放这些内存。这是因为操作系统会负责处理内存的回收工作。

我很难理解为什么alpha通道需要进行sRGB转换,以及为什么你最初的代码不正确。alpha通道只是表示来自一个光源和另一个光源的光子比例。那不是与亮度无关的颜色曲线的范围吗?

首先,alpha 通道并没有进行 SRGB 转换。系统中,alpha 通道保持为线性值,并没有像颜色通道一样进行转换。Alpha 通道用于表示来自不同光源的光子比例,因此它与亮度无关,也不涉及颜色空间的转换。因此,当前的系统中,alpha 通道没有被转换为 SRGB 值。

为什么你写的是 ‘Texel.a + Dest.a - Texel.a * Dest.a’,而不是 ‘InvRSA * Dest.a + Texel.a’,这样就可以与RGB通道匹配吗?

没有特别的原因,只是因为在黑板上算过数学,结果就是这样。如果记得没错的话,就是这样写出来的。
在这里插入图片描述

重排列这些计算,继续合并

通过重新安排代码,可以使其更加简洁清晰。原本的公式可以通过移位和简化变为一个更加易于理解和阅读的形式。特别是将 Texel.aDest.a 的计算方式优化成 (1.0f - Texel.a) * Dest.a,使得公式更简洁。此外,通过识别出数学上的等价关系,也可以进一步简化代码,比如通过将公式重写成 in * essay * dust + texel 来让代码更具可读性。

这种简化并没有改变代码的结果,而是使得它变得更加干净、易懂。在此基础上,甚至可以将重复的操作进一步精简,例如同样的代码通过移位和识别相同操作简化,最后得到更加简洁的公式。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

通道不是使用非线性颜色来创作的吗?

通常情况下,alpha 通道并不是以非线性色彩进行编写的。alpha 通道通常是通过 Photoshop 等工具在绘制时根据每个像素的覆盖值来生成的。因此,假设 alpha 通道不是以 sRGB 编码的,尽管没有 Photoshop 的源代码来确认这一点。

后续任务

明天我们会继续,或许会开始考虑如何在前进的过程中保持代码的优化,同时可能会分离一些代码,整理好以后再继续进行渲染部分的工作。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com