欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 新车 > 游戏引擎学习第128天

游戏引擎学习第128天

2025/3/2 0:53:03 来源:https://blog.csdn.net/TM1695648164/article/details/145930423  浏览:    关键词:游戏引擎学习第128天

开始

然而,我们仍然有一些工作要做,渲染部分并没有完全完成。虽然现在已经能够运行游戏,而且帧率已经可以接受,但仍然有一些东西需要进一步完善。正在使用调试构建编译版本,虽然调试版本的性能不如优化版本,但仍然可以流畅运行游戏。

接下来,需要做一些调整和改进。渲染器中有一些还没有完成的部分,可能会对整体表现产生影响,需要注意。为了做好准备,写了一些待办事项,主要是关于渲染器的一些优化,比如确保所有的瓦片都能够正确地对齐缓存行。虽然目前的实现已经足够好,但仍然有一些细节上的优化空间,尤其是在缓存管理方面,确保能够最大化性能。

这些改进并不需要立刻完成,因为目前的系统已经相对稳定,可以继续处理这些问题而不会影响到其他部分的代码。因此,接下来的一段时间可以用来进行这些优化,进一步提升渲染器的效率和性能。

总的来说,当前进展顺利,渲染器的速度令人满意,但仍需进行一些细节上的优化和调整,确保最终的效果更好。

黑板:对齐到缓存行

在开发过程中,讨论到了一个缓存相关的问题,特别是如何确保瓦片(tile)在内存中的对齐,以提高渲染性能。当前我们将屏幕分成瓦片并将其分配给不同的线程进行处理,目的是使得每个线程可以独立渲染不同的瓦片,从而加速渲染过程。例如,核心0负责一个瓦片,核心1负责另一个瓦片,依此类推。

然而,即使我们已经保证每个瓦片的内存位置是对齐的,避免了线程间的直接竞争,仍然存在一个潜在的问题,即“缓存线竞争”(Cache Line Contention)。所谓缓存线,就是处理器缓存的最小单位。如果两个相邻的瓦片跨越了同一个缓存线,那么当一个核心修改了缓存线的数据,另一个核心的缓存中的相同数据将被标记为无效,从而需要重新加载数据。这种数据的“争夺”可能导致性能下降,因为核心1必须重新从核心0的缓存中读取数据。

为了防止这种情况发生,理想的做法是确保瓦片的边界刚好落在缓存线的边界上,这样每个核心就能独立处理自己的瓦片而不会互相干扰。如果瓦片的边界没有对齐缓存线,可能会导致缓存行在核心之间来回传递,增加内存流量和缓存无效的次数,最终影响性能。

因此,为了避免可能的性能瓶颈,需要进行优化,确保瓦片的内存布局与缓存线对齐。这可能会带来显著的性能提升,尤其是在多核心处理时。虽然目前还没有出现明显的性能问题,但这个问题仍然需要关注和测试,以确保不会影响最终的渲染效率。

渲染器待办事项

在开发过程中,讨论了几个优化渲染性能的方向,特别是如何利用超线程和调整瓦片大小来提升渲染效率。

首先,考虑到超线程(Hyper-Threading)技术,目前系统并没有充分利用超线程来提升并行性。虽然系统支持超线程,但并未专门针对其进行优化。超线程允许每个核心同时执行两个线程,理想情况下,可以将任务分配给这些线程,以便更高效地利用CPU资源。不过,当前的问题在于如何同步这些线程的工作,使得超线程能有效地交替工作,确保每个线程处理不同的任务,而不会产生冲突或数据共享问题。由于目前没有明显的同步机制,暂时还没有实现对超线程的优化。未来可以通过研究和与他人讨论,探索如何充分利用超线程,以提高渲染效率,或者如果这个思路不行,也可以放弃这个方案。

接下来,讨论了瓦片(Tile)大小对性能的影响。瓦片的大小决定了在渲染过程中每个瓦片能占据多少缓存。如果瓦片太大,可能会导致大量数据无法全部加载到缓存中,从而降低效率。而如果瓦片太小,虽然每个瓦片能更好地利用缓存,但每个瓦片的处理开销也会增加,尤其是在设置阶段,需要处理更多的块、裁剪等工作。因此,瓦片大小的选择是一个需要权衡的点,既要保证性能,又要考虑到每个瓦片的处理成本。未来需要进行测试,找出一个合适的瓦片大小,以优化渲染过程。

另外,提到需要对渲染流程的内存带宽进行评估,以了解当前的内存使用效率。需要通过实际测试来计算内存带宽,以判断是否存在瓶颈,以及是否可以通过调整内存访问方式来提高性能。

最后,讨论了对指令选择的重新测试。现在系统已经支持多线程渲染,因此需要重新评估现有指令的选择,确保在多线程环境下,指令执行能最大限度地提高效率。通过分析和测试,进一步优化指令集,以提升整体性能。

尽管有这些优化方向,但目前还不打算马上实施这些改进,而是希望在稍后进行更深入的性能测试和调整,找出最合适的优化方案。

今天的工作:解决剩余的问题

接下来,讨论了渲染系统的清理工作。在渲染功能已经可以运行并且性能良好的基础上,目标是进一步完善渲染的功能,并确保渲染系统在最终优化之前具有完整的特性。主要目的是避免在不完全理解系统的情况下对渲染进行过早的优化。

首先,需要整理和修正坐标系统。虽然之前进行了一些优化,并且渲染系统的运行速度已经非常快,但仍然需要进行一个坐标系统的整理,以确保各部分的功能和结构合理。之后,还需要继续完成一些关于光照的工作,进一步优化渲染性能,最终可以将渲染部分标记为完成。

除了光照,另一个需要考虑的内容是粒子系统。粒子系统在渲染中通常具有不同于其他元素的特点,因此需要专门考虑如何将其与渲染系统集成。粒子系统的开发可能会在光照优化前或后进行,具体顺序尚不确定,但粒子系统也是渲染优化的一部分。

完成上述工作后,计划进行一次最终的组织整理,然后可以将渲染部分视为基本完成,为游戏的开发和发布打下基础。渲染系统的优化和完成将意味着进入下一阶段的游戏开发,准备逐步推进其他方面的工作。

此外,还需要考虑游戏中的音频和动画系统的整合,并将这些工作安排在接下来的开发计划中。音频和动画相关的内容将逐步融入到游戏中,并且这部分将进入更高层次的游戏功能开发。

总结来说,当前的工作重点是完成低层引擎部分的开发,并尽快完成渲染系统的最终整理和优化。这些低层工作完成后,将为接下来的游戏开发提供更坚实的基础,使得开发团队能够顺利向游戏的其他方面过渡,并最终推动游戏的发布。

查看坐标系统

在渲染系统的开发过程中,渲染实体的处理和相机系统的管理显得有些混乱。最初,我们将渲染任务简单地推送到一个列表中,但这样做显得有些草率,并且没有认真考虑这些任务应该如何有序地工作。具体来说,渲染实体的处理方式让人感觉有些繁琐,包含了许多不必要的操作,这些操作可能并非渲染过程中必需的。

相机的处理也是如此,感觉我们在操作相机时过于随意,没有遵循一个标准的三维渲染管线做法,这让系统的行为显得不够清晰和可控。因此,计划对这些部分进行重新审视和优化,目的是使其更加符合标准的三维渲染管线结构,这样能够更好地理解和控制渲染过程中的每一个环节,确保系统的稳定性和性能。

重新整理和优化渲染实体的处理和相机系统的管理将帮助系统更加高效和稳定,也能为后续的开发和调试打下更好的基础。
在这里插入图片描述

重新审视基础变换

目前渲染过程中存在一些问题,尤其是在三维变换的处理上。虽然渲染大部分内容是在二维空间中进行的,但实际上,很多操作仍然依赖于三维变换。我们目前面临的一个问题是没有有效的机制来避免不必要的三维操作,尤其是在处理地面区块时。这导致我们无法确保在正确的像素空间进行操作,进而影响了地面区块的无缝拼接。

为了更好地解决这个问题,计划将渲染的变换过程进行改进。目的是确保可以在两种模式下灵活处理变换:一种是标准的正交变换(如最初的像素空间变换),另一种是透视变换。为了实现这一点,打算不再延迟变换的执行,而是将其直接在需要的位置进行。这一改变需要调整当前的实体模拟方式,避免在实体模拟后才进行渲染处理。

具体来说,当前的流程是在实体模拟时执行其对应的操作,然后等待模拟结束后再渲染图形。但这种做法存在问题,因为直到模拟完成后才能知道实体的位置,导致渲染的时机被延后。因此,为了改进这一流程,打算在实体移动时直接进行相应的渲染变换,从而避免不必要的延迟和复杂性,使整个渲染过程更加高效和流畅。

通过这些改进,希望能使渲染系统更清晰、更具可控性,同时避免冗余的三维变换和延迟渲染,从而提高整体的渲染性能和灵活性。

拆分更新/渲染

渲染敌人的过程可以通过两种方式实现。为了简单起见,当前选择了最直接的方式,但将来可能会做一些调整。目前的做法是复制现有的switch语句,并将其一部分用来进行渲染,另一部分用于实体模拟。这样,渲染和实体模拟分开处理,渲染部分负责绘制图像,实体模拟部分负责计算实体的行为和状态。

计划在未来可能会将这两部分合并处理,尤其是在移动方式上有所改动时。也可能会重新命名这些操作函数,使得它们更符合实际的工作内容,比如将“模拟实体”的部分重命名为“自由物理模拟实体工作”之类,以更清晰地表达每个函数的作用。

目前,为了保持简单,决定先按现有的方式将代码拆分,渲染部分和模拟部分各自独立执行。这一过程包括移除一些不必要的代码,比如一些不需要调用的发送操作。删除这些后,剩下的代码只涉及到需要渲染或模拟的实体,减少了不必要的复杂性。

通过这种方式,渲染和实体处理将更清晰,实体将被渲染到它们实际的位置,而不再是之前那种依赖模拟后再进行渲染的方式。这使得渲染和实体处理更加直接和高效,能更精确地控制每个实体的状态和位置。
在这里插入图片描述

按需执行基础变换

为了避免延迟执行的复杂性,现在决定将转换操作直接在线上进行处理,而不是事后再进行调整。具体来说,首先通过修改渲染组的默认基础设置,使得它可以直接被使用和覆盖。这意味着,所有渲染和变换的设置将直接嵌入到渲染组中,而不需要单独的延迟步骤。

首先,要提取出渲染所需的基础数据,比如屏幕坐标、摄像机的距离和焦距等,所有这些都将被直接放入渲染组中进行处理。这一过程中,摄像机相关的设置(例如渲染相机)将移除,统一由渲染组来处理。这种调整将使得渲染和变换操作更加直接和清晰。

此外,调整后,渲染过程中的每个操作(如推送位图或矩形)都会先进行基础变换,再检查变换后的物体是否在视图范围内。如果物体超出视图,它将不会被推送到渲染队列中,这样避免了不必要的渲染计算。

对于坐标系统,也同样调整为在操作时实时计算,而不再是后续再进行补充。这样可以减少不必要的计算和数据存储,提升渲染效率和清晰度。

总之,通过将这些处理步骤提前并简化为实时操作,避免了延迟计算和数据存储,提高了系统的整体效率。这些改动让渲染和变换过程更加直接,减少了复杂度,同时提升了渲染的实时性和精度。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

去除EntityBasis

在处理渲染实体时,决定简化和优化数据结构,去掉了一些冗余的字段。首先,原来存储的“scale”(缩放)字段实际上没有被使用,因此决定移除它,只保留了“p”以及与大小相关的必要信息。对于渲染位图,原本存储的“size”和“scale”字段不再需要,因为它们对当前渲染流程没有实际作用。只有位置和大小信息会被保留。

在考虑是否需要存储更多的信息时,发现仅当涉及到照明计算时,才有必要存储“像素到米”的转换比例,因为这一信息会影响到照明计算。对于不涉及照明的对象(例如渲染实体位图),则不需要存储这个转换比例。这样,系统可以根据实际需求决定是否存储这些额外的转换数据。

为了处理照明问题,当渲染实体需要参与照明计算时,会特别存储“像素到米”的转换比例,而其他对象则不需要。对于坐标系统,可能也需要存储这一比例,以便正确计算光照效果。

在进一步分析和整理时,去除了冗余的字段和不必要的转换步骤。例如,原本为矩形绘制设置的“pixels to meters”字段,现仅在需要照明的情况下保留。其他不涉及照明的情况下,这一字段被默认设置为1,表示没有进行任何转换。

最终,通过将不必要的数据移除和简化存储结构,渲染代码变得更加高效,减少了内存占用并简化了计算过程,特别是在不涉及照明的情况下,进一步提高了渲染效率。
在这里插入图片描述

在这里插入图片描述

初始化变换

在这段处理中,主要是对渲染变换(transform)的初始化进行了一些调整。之前的默认变换(DefaultBasis)已经移除,但我们现在需要确保渲染变换对象正确初始化,并且包含必要的字段,例如缩放(Scale)和偏移(OffsetP)。这些字段将成为渲染变换的核心部分。

初始化渲染变换:
在新的变换结构中,首先需要将一些必要的参数加入进来,包括:

  • 缩放(Scale)
  • 偏移(OffsetP)
  • 焦距(FocalLength)
  • 目标上方的距离(DistanceAboveTarget)
  • 显示器的半宽度(MonitorHalfDimInMeters)
  • 像素到米的转换(MetersToPixels)

这些数据在渲染过程中是必须的,用于计算和显示正确的渲染效果。

问题解决:

  • 渲染变换的初始化:将变换结构初始化为无效状态(或者合理的初始值),确保它在后续处理中能够正确工作。
  • 移除不再需要的字段:由于默认变换已被移除,某些冗余的代码也被去除。例如,渲染组直接包含变换对象,不再需要通过间接方式处理。

关于显示器设置:

  • 显示器的半宽度(MonitorHalfDimInMeters)在变换结构中仍然需要处理,因为它直接影响到渲染时的视角设置,因此没有被移除。
  • **全球透明度(GlobalAlpha)**仍然是需要保留的,这与渲染中的淡入淡出效果有关。

总结:
经过调整,所有渲染变换相关的内容都被集中到变换结构里。所有必要的参数都被初始化并正确设置,同时去除了一些不再需要的字段。为了确保计算正确,变换初始化时需要根据显示器尺寸、焦距、目标距离等信息来设置。这些更改使得渲染系统更加简洁高效,并且避免了冗余的计算和存储。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

翻译调用

在这段代码中,主要的目的是对渲染变换(transform)进行重新组织和优化,确保渲染的操作在内存中高效执行,并且简化了对一些参数的处理。整体思路是通过将变换相关的数据集成在渲染变换结构中,避免了不必要的中间步骤和冗余的数据存储。

变换结构的调整:

  1. 屏幕中心(ScreenCenter)加入到变换中
    之前的代码中,屏幕中心信息是单独管理的,而现在它作为变换的一部分,直接包含在变换结构中。这样可以简化处理过程,直接从变换中获取所需的信息。

  2. P值和偏移(OffsetP)
    在渲染过程中,实体位置的计算依赖于P值。原先的 EntityBasis 主要是用来获取P值,而现在直接通过传递渲染变换中的P值来简化操作。P值包括了三维空间中的位置,而偏移则是通过变换中的OffsetP来处理的,因此原来的 EntityBasis 可以被移除。

  3. 调整P值
    为了确保位置正确,P值需要根据变换中的偏移进行调整。每次计算P值时,都需要考虑变换的偏移,这样可以实现位置的正确调整。例如,在计算渲染位置时,P值要加上偏移量,确保显示的正确性。

  4. 去除不必要的字段
    之前的代码中存在一些不再需要的字段,比如显示器的半宽度(MonitorHalfDimInMeters)已经不再需要,因此被删除。我们现在只保留必要的字段,如缩放比例、偏移量和焦距等。

  5. 像素与米的转换(PixelsToMeters)
    PixelsToMeters 仍然是一个需要保留的参数,因为它在渲染和光照计算中使用。如果渲染项没有涉及光照,它可以不被使用,但是对于涉及光照的物体来说,PixelsToMeters 是必不可少的。

  6. 渲染流程简化
    通过将所有变换计算直接嵌入到渲染过程中,避免了等待或者延迟的计算。这样每次渲染时,都会直接计算出最终的P值,避免了后续重新计算带来的开销。渲染过程中的每一步(如矩形绘制、位图推送等)都会按照这种新机制进行。

  7. 大小(Size)和缩放(Scale)处理
    之前可能对每个渲染项都处理了缩放(Scale)和大小(Size)分离的操作,现在直接将缩放参数内嵌到大小值中,确保在渲染时可以直接使用正确的缩放后大小,而不需要单独存储和计算缩放。

  8. 调整后的渲染逻辑
    渲染操作中,首先检查渲染项是否有效,如果有效,再根据变换后的P值计算实际的显示位置。然后通过新的渲染方式,直接应用变换并计算出正确的P值,避免了过多的冗余存储和计算。

总结:

整体优化过程中,重点在于简化渲染变换的计算流程,移除不再需要的中间步骤,避免冗余的存储和计算。变换数据直接嵌入渲染流程中,使得每个渲染项都能在执行时直接计算出正确的P值,并且通过合适的偏移和缩放进行调整。这样,渲染系统的性能得到了提升,代码结构也变得更加清晰和高效。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

完成代码修改

在这段代码的修改过程中,核心目标是简化和优化变换和渲染基础结构的管理。主要的操作包括调整如何传递和使用变换数据,去除不再需要的冗余代码,并确保渲染组(RenderGroup)能够正确地处理变换。

主要修改内容总结:

  1. 变换的优化

    • 现在不再需要使用“调试摄像机”(DebugCamera)或者其他不必要的调试工具。原本需要多次计算的变换,现在通过直接传递和使用渲染组的变换来完成。所有的变换计算被简化为直接应用当前的变换,并根据目标位置和偏移量调整。
    • 其中涉及到的变换(如目标距离等)不再单独处理,而是通过调整变换数据结构来完成。这样做的目的是减少冗余计算,提高代码效率。
  2. 渲染基础(RenderBasis)移除

    • 原本的 RenderBasis 处理逻辑被简化,不再需要额外的渲染基础类。现在只需要将相关的大小(size)直接传递给渲染组,避免了不必要的数据结构操作。
    • 渲染组的变换可以直接从变换数据中提取出来,不再依赖原本的渲染基础类。去除了之前的 RenderBasis 相关的代码,清理了冗余部分。
  3. 移除不必要的初始化操作

    • 渲染组中不再需要重复初始化渲染基础数据(如位置、大小等)。原本的代码中需要将数据推送到渲染队列,但现在直接传递所需的变换数据即可,不再涉及不必要的初始化和处理步骤。
    • 在原本需要设置渲染基础数据的位置,现在只需要设置渲染组的变换参数,并通过调整位置和偏移量来正确定位渲染物体。
  4. 清理冗余字段和操作

    • 删除了一些不再需要的字段和处理逻辑。例如,渲染基础位置的设置和偏移量的调整,现在由渲染组的变换和目标位置直接管理,而不再需要额外的计算和设置。
    • 通过去除这些冗余代码,简化了渲染和变换流程,使代码更简洁易懂。
  5. 错误处理和调试

    • 在进行代码修改时,遇到了一个关于渲染组变换(RenderGroup.Transform)的错误。错误提示指出,渲染组的变换必须是一个类实例,而不是一个指针。经过检查,发现需要正确设置渲染组中的变换类型和指针,解决了这个问题。
  6. 未来计划和优化

    • 当前的修改已经基本完成,下一步是继续优化摄像机的设置和渲染组的使用,确保变换数据和渲染数据的高效结合。
    • 尽管现在的代码已经简化,未来可能还需要调整渲染组的设置方式,确保其能够更灵活地适应不同的渲染需求。

总结:

整体上,修改的核心是优化和简化渲染和变换的流程,通过直接使用变换数据来减少不必要的中间步骤,去除冗余的渲染基础结构,提升了代码的可维护性和执行效率。变换和渲染基础的调整使得每次渲染操作都更加直接和高效。同时,解决了相关的调试错误,确保了渲染组和变换的正确连接。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

什么都没有黑屏调试一下

在这里插入图片描述

在这里插入图片描述

PushBitmap 没调用
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

测试更改

在调试中使渲染变换生效

考虑到调试过程中的变换操作,想要尝试一种新的方法:在调试模式下,变换可能会以不同的方式工作,主要是希望能够直接对变换进行修改,并且在调试过程中,变换的修改可以是后置的、事后才会应用的。具体来说,考虑通过设置“DistanceAboveTarget”来调整调试相机的位置,并且在某些情况下,可以在调试过程中让这个距离数值更高,或与常规操作下的距离不同。

一种可能的实现方式是,在调试相机模式下,调整“DistanceAboveTarget”值,可能会使得相机距离目标更远或更近,从而影响相机视角,或者甚至完全改变其视距。这种改变可以在调试过程中进行,而不影响正常模式下的表现。例如,可能通过代码控制将相机距离的变化与调试状态挂钩,只有在调试状态下,这些变动才会生效,从而避免干扰到正式发布的功能。

不过,对于这种方法是否真的有效,还没有完全确定。考虑到需要在调试中调试视角并获得精准的数据,或许这种调整可以帮助开发过程,但在实际操作时可能还需要进一步考虑其复杂性和可行性。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

一切第一次工作是因为你在大声思考。

有时候,虽然我们在系统中加入了很多功能,但并不是每个功能都会在第一次运行时就能完美工作。通常,意料之外的地方可能会出现问题,而一些自己预料到会出问题的地方,反而有时却能顺利运行。这种情况挺奇怪的。可能是因为当我们觉得某个功能很复杂时,我们会更加专注和小心,避免出错。而那些觉得比较简单或者没那么重视的地方,反而可能会出现意外的错误。

黄色部分代表加载的位图吗?

黄色部分表示的是加载的位图,但黄色本身只是为了方便调试,展示了目前哪些部分没有正常渲染出来。它主要是用来标记地面区域的填充情况。原本这些黄色标记只是调试用的,帮助看到哪些地方没有被正确填充。现在可以考虑把这些标记重新打开,甚至在明天的工作中,可能会把它们永久保留,因为不再需要为调试目的而关闭它们。

目前,剩下的核心工作就是处理一些与深度排序、照明和如何处理对象的深度(z轴偏移)有关的问题。这部分内容是引擎中的大问题,尤其是在处理这些与深度和排序有关的功能时,还需要解决如何与光照系统协调工作的问题。具体来说,处理这些问题是当前待办事项中唯一的“大问题”。

另外,关于2D处理,当前出现了一个问题,就是在处理深度偏移时,把所有的偏移量都当作一样对待了,但显然不应该这样做。比如,现在可以看到角色的头部在上下浮动时,它也会左右移动,这种现象本来是不想发生的,但时不时会有些犹豫,不确定是否应该这么处理。总的来说,关于这个问题,需要进一步探讨,明天可以继续看看该如何调整。

有些现阶段的占位符是为了未来的楼梯设计而放置的,目前还不确定最终会如何处理这些占位符。理想的效果是让这些对象能稳定地堆叠在一起,保持正确的层级关系,虽然目前还不确定如何做到这一点,但这确实是一个棘手的挑战。

然而,最让人兴奋的是缩放功能。对缩放效果的喜爱让大家决心一定要找到解决方案,确保游戏中的物体在走楼梯时会根据玩家的高度变化进行缩放,视觉效果非常酷,大家觉得这个功能必须要有。所以,尽管处理起来有些难度,但一定要想办法解决,并且与美术资源协调好。

黑板:缓存失效

在处理缓存时,首先需要了解 CPU 中的缓存是如何工作的。每个核心(Core)都有自己的缓存,这些缓存用于存储数据,以提高访问速度。缓存比主内存要小得多,但它们的作用是减少 CPU 访问主内存的时间。

  1. 数据加载
    假设我们有一个位置 A 的数据存储在内存中。当 CPU 核心(比如核心 0)需要加载这个数据时,它会先检查自己的缓存。如果缓存中没有这个数据,它就会向主内存请求数据。当主内存将数据提供给 CPU 后,数据会被加载到核心的缓存中,并可能标记为 “共享” 或 “独占”(具体取决于缓存的一些标记)。

  2. 多个核心共享数据
    如果另一个核心(比如核心 1)也需要访问位置 A 的数据,它会先查询自己的缓存。通常它会通过 “嗅探” 操作(snooping)检查其他核心是否已经有了这个数据。如果核心 0 已经缓存了这个数据,核心 1 会从核心 0 的缓存中读取数据,而不是再次去主内存请求。这时,两个核心都共享这块数据。

  3. 数据修改与缓存失效
    问题出现在当某个核心(例如核心 0)需要修改这块数据时。如果核心 0 正在修改数据,它需要先通知其他核心它将独占这块数据。为了确保数据一致性,核心 0 会将这块数据的缓存标记为 “独占”(exclusive),同时通知其他核心标记该数据为无效(invalid)。这时,其他核心不能再继续使用这块数据,必须重新从内存或核心 0 的缓存中获取最新的数据。

  4. 写入操作与缓存一致性
    当核心 0 完成写入操作后,它将数据标记为 “修改”(modified),而其他核心必须清空它们的缓存中原来的数据,以确保所有核心都能获取到修改后的数据。这是缓存一致性协议的一部分,确保所有核心的缓存数据是同步的。

  5. 缓存失效的过程
    所谓 “无效化” 缓存,就是在数据被修改后,某个核心的缓存中的数据不再是最新的,必须标记为无效。这是因为其他核心可能在背后修改了数据,从而导致该缓存行的数据已经不再正确。每当发生写操作时,缓存行会被标记为 “无效”,并且其他需要该数据的核心必须重新获取。

总结来说,缓存失效是在多核 CPU 中保持数据一致性的一个关键步骤。它确保了当多个核心操作相同的数据时,能够通过有效的缓存协议保证数据的准确性和一致性。

能否总结一下今晚做了什么?有些地方有点难以跟上。

今天做的主要工作其实比较简单,涉及到一些渲染流程的改动。之前的做法是,我们有一个渲染缓冲区(render buffer),然后会推送一个叫做“实体基准”(entity basis)的变换。接着,使用这些变换来处理位图记录(bitmap records),这些记录定义了要绘制的位图以及相关的基准数据。

在渲染时,我们会结合这些基准数据和位图数据,来计算出位图的实际位置和屏幕上显示的矩形区域。也就是说,变换和位图数据是分开处理的,变换数据仅在渲染时才会结合起来。

不过,我们对这个过程做了一些改动,目的是提高效率。改动后的做法是,在渲染组(render group)中直接存储当前的变换矩阵,而不再推送基准数据。然后,在渲染缓冲区(push buffer)中,我们不再推送基准数据,而是直接推送位图。当位图被推送时,我们会立即在推送时进行变换计算,而不是等到渲染时再计算。这样,我们直接存储变换后的结果,而不是存储所有需要重构数据的中间信息。

这样做的主要目的是提高效率,避免延迟执行变换,而是在数据推送时就完成变换操作。这就是今天的主要工作,简单来说,就是将变换从延迟执行改为推送时立即执行。

你能解释一下什么叫做串行处理吗?

当说某个操作是按串行(serial)进行时,意味着这些操作必须按顺序执行,一个接着一个,没有重叠。也就是说,每个操作都得等前一个操作完成后才能开始,不能并行执行。比如,假设有四个步骤:A、B、C 和 D,按串行执行时,它们会依次执行:

  • 第一步:执行 A
  • 第二步:执行 B(A 完成后才能开始)
  • 第三步:执行 C(B 完成后才能开始)
  • 第四步:执行 D(C 完成后才能开始)

每个步骤只能在前一步完成后才能开始,换句话说,所有的操作都得“排队”,因此这些操作在时间上是连续发生的。

并行(parallel)执行则是完全不同的。并行意味着这些操作可以同时进行,不需要等待其他操作完成。举个例子,假设我们仍然有 A、B、C 和 D 四个步骤,在并行执行的情况下,所有的步骤都可以同时开始:

  • 在同一时刻,A、B、C 和 D 都开始执行,尽管它们可能花费不同的时间,但它们不需要等待其他步骤完成就可以一起开始。

串行执行的时间是所有步骤所需时间的总和。也就是说,若每个步骤需要的时间分别是 tA、tB、tC 和 tD,那么总时间就是 tA + tB + tC + tD。

并行执行的时间则是所有步骤中花费时间最长的那个。也就是说,假设 A 需要 tA 时间,B 需要 tB 时间,C 需要 tC 时间,D 需要 tD 时间,那么并行执行的总时间是 max(tA, tB, tC, tD),即取最慢的步骤的时间作为整个过程的执行时间。

总结来说,串行需要所有操作按顺序逐个执行,整体耗时较长。而并行则能让操作同时进行,整体耗时则取决于执行时间最长的那个步骤。

在你移除缩放后,我错过了一部分内容。难道你不需要它来做Z轴上的缩放吗?

在这个过程里,之前使用的缩放(scale)功能被移除的原因是因为不再需要在渲染过程中存储它。原先的做法是为了根据 Z 轴来进行缩放,但现在的做法是通过直接计算缩放值来替代存储缩放因子。

具体来说,渲染转换(render transform)中设置了缩放值,但这个值并不再被存储到渲染缓冲区中。每次进行推送(push)时,都会在推送时计算出当前的缩放值,并且直接存储计算后的尺寸,而不需要存储缩放因子本身。

例如,计算缩放时,会在推送时直接处理这个缩放,而不在渲染数据中保留缩放因子。计算完成后,所有相关的内容,包括位置(p),都会被正确地缩放,并且只存储这些最终的值,而不再存储原始的缩放因子。这样做的目的是提高效率,避免不必要的存储,因为在推送后,缩放因子已经没有必要再被使用。

在位图之间的阶梯通过是什么情况?是基于Z轴的简单缩放吗?

在这段过程中,涉及到的是如何在渲染和缩放时处理 Z 轴的偏移问题。最初,p 值被用于计算偏移,这个值主要影响 X 和 Y 轴的偏移。然而,Z 轴的处理需要进行调整,p 值不应该影响 Z 轴的偏移。

具体来说,目标是消除 Z 轴的偏移(即,不再使用 p 的值来直接调整 Z 轴)。在进行转换时,首先需要应用 p 的变换来影响位置,然后再考虑缩放。在这一过程中,缩放是基于 p 值和 Z 轴的偏移量进行计算的。这个偏移量可能会影响 Z 轴的最终位置,但需要在渲染计算后才加上。

目前遇到的问题是,虽然已经调整了 Z 轴的偏移,但仍然发现 Z 值可能没有完全“归零”或清除,所以在实际操作中还是可能有一些偏移。暂时的解决方法是将 Z 轴的偏移设置为零,并计划在未来继续调整和优化如何正确地处理这个问题。

简而言之,目标是修正 Z 轴偏移的处理方式,确保缩放和变换仅影响 X 和 Y 轴,并且将 Z 轴的偏移在渲染时进行正确的计算和调整。

这个的FPS是多少?

目前,运行的帧率是每秒 60 帧(FPS),并且是固定的。如果将编译器切换到优化模式,性能会更好,达到更高的效率。

不过,地面块的重建(ground chunk rebuilding)目前还没有在单独的线程中处理,虽然理论上可以将其独立出来,这样可以避免一些性能问题。当前,仍然会在某些情况下出现轻微的卡顿,比如移动到新位置时会看到一些小的“卡顿”现象。这个问题需要解决,虽然整体来说,性能表现还算不错,进展比较顺利。

为了创建阶梯的3D效果,是否需要创建一种渲染技术来绘制阶梯的墙壁,从底楼开始,到顶楼结束?

为了实现楼梯的三维效果,目的是从底层开始绘制墙面,直到顶层,形成一个很酷的视差效果。这需要设计一个渲染技术来处理楼梯墙面的绘制,确保在不同的高度上呈现出良好的效果。具体要怎么做,还没有完全确定,可能会在之后进行调整和改进,计划会根据实际情况进行优化和实现。
在这里插入图片描述

在这里插入图片描述

什么时候会考虑咨询其他程序员?什么情况才到达这个门槛?

在考虑是否向其他程序员请教时,通常是在自己遇到某个自己不擅长的领域时,特别是当我知道某个程序员在某个领域非常专业时。例如,涉及到多线程编程时,如果自己对这一部分不够精通,可能会向一些擅长这个领域的程序员请教,了解他们的思路和方法,获取一些有价值的见解。通常不太会单纯为了讨论编程问题而去咨询别人,因为大部分情况下自己能处理这些问题。但如果遇到自己不熟悉的领域,或者对某些技术有疑问时,会主动请教经验丰富的人。这不仅是为了当下的项目,也是为了以后能积累更多的经验和知识。

如果你想对场景的不同部分应用不同的变换或光照,会发生什么?

如果需要对场景中不同部分应用不同的变换,其实系统已经为此做好了准备。当前,在处理每个实体时,已经有不同的变换应用到了每个实体上。所以,如果需要对场景的不同部分进行不同的变换,这个操作是非常简单的。实际上,现在系统已经在每个实体上应用了变换,其中每个实体的变换都可以根据其位置来设置。如果需要为每个实体设置不同的焦距或其他任何参数,也是可以轻松做到的。总的来说,系统已经支持了这种灵活的变换应用,可以根据需求进行不同的设置和调整。

你预期使用OpenGL/D3D进行Blitting会有多少性能提升?

使用OpenGL进行处理时,主要的性能提升来自于硬件方面的优化。OpenGL提供了更多的硬件资源来处理内存复制,这些硬件资源本身就为这种操作进行了优化,因此在处理纹理时,OpenGL应该能比当前的方式更快速。如果指的是将数据展示到屏幕上的过程,那么通过OpenGL进行渲染可能会更加流畅,因为没有经过一些复杂的路径。不过,对于具体提升的幅度,目前还不太确定,可能提升的程度并不会特别大。

渲染器中是否有类似于OpenGL/D3D中对模型空间/摄像机空间/裁剪空间的翻译的概念?

在OpenGL中,模型空间(local space)、世界空间(model space)、相机空间(camera space)和裁剪空间(clip space)之间的转换是非常关键的,但OpenGL本身并不直接处理或关心这些转换。它只关心裁剪空间,并且允许开发者自己在着色器中进行所有必要的变换。具体来说,OpenGL并不直接计算从局部空间、模型空间到相机空间的转换,而是将这些变换组合起来,在着色器中计算最终的裁剪空间坐标。

在这种方式下,OpenGL让开发者完全控制所有的变换过程,而无需显式地处理每一个空间转换。相反,其他图形API或渲染系统(比如DirectX)可能更关注这些转换步骤,特别是如何将对象从局部空间转换到裁剪空间,进行相应的计算和处理。

总之,OpenGL并不关心局部空间、模型空间或相机空间等中间步骤,而是通过在着色器中完成所有必要的计算,最终将对象转换为裁剪空间。

我找到一个你讲解四元数双重覆盖的视频,但没有解释四元数。你会考虑在不久的将来做一个解释吗?

关于四元数双重覆盖(quaternion double cover)的问题,目前并不打算提供这方面的讲解,因为已经很久没有深入研究四元数了,尤其是自从最初接触时,四元数在计算机图形学中还比较新颖。虽然四元数的数学背景本身并不新,早在哈密顿时代就已经有相关理论,但随着时间的推移,四元数的应用和理论也有了很多发展,尤其是现在的双四元数(dual quaternion)等技术,已经超出了以前的研究范围。

四元数的应用如今涉及到许多复杂的几何代数内容,这些内容并不熟悉,也没有进行过深入的学习,因此无法为大家提供一个完整的入门讲解。四元数的学习不仅仅是理解其数学背景,还需要涉及一些现代几何代数的概念,而这些内容并没有深刻了解。因此,可能需要找到更合适的专家来进行详细的讲解。

总的来说,四元数虽然是一个重要的数学工具,特别在计算机图形学中用于旋转等操作,但如今的技术发展已经将其应用范围拓展,理解和使用现代的双四元数等技术,也要求对更广泛的几何代数有所了解。

1. 什么是四元素(Quaternion)?

四元素是一种数学对象,用于表示三维空间中的旋转。它由威廉·哈密顿(William Hamilton)在1843年提出,形式为:
q = w + x i + y j + z k q = w + xi + yj + zk q=w+xi+yj+zk
其中:

  • w , x , y , z w, x, y, z w,x,y,z 是实数;
  • i , j , k i, j, k i,j,k 是虚数单位,满足 i 2 = j 2 = k 2 = i j k = − 1 i^2 = j^2 = k^2 = ijk = -1 i2=j2=k2=ijk=1

四元素可以看作一个标量( w w w)加上一个三维向量( x i + y j + z k xi + yj + zk xi+yj+zk)。在计算机图形学、机器人学和航空航天中,四元素常用来表示旋转,因为它比旋转矩阵更紧凑,且避免了“万向锁”(gimbal lock)问题。

例子:

假设一个四元素 q = 0.707 + 0.707 i + 0 j + 0 k q = 0.707 + 0.707i + 0j + 0k q=0.707+0.707i+0j+0k,它表示绕 x x x 轴旋转 90 度的旋转。

  • w = cos ⁡ ( θ / 2 ) w = \cos(\theta/2) w=cos(θ/2) θ \theta θ 是旋转角度,这里 θ = 9 0 ∘ \theta = 90^\circ θ=90,所以 w = cos ⁡ ( 4 5 ∘ ) = 0.707 w = \cos(45^\circ) = 0.707 w=cos(45)=0.707
  • 向量部分 ( x , y , z ) = ( 1 , 0 , 0 ) (x, y, z) = (1, 0, 0) (x,y,z)=(1,0,0) 表示旋转轴,模长标准化后乘以 sin ⁡ ( θ / 2 ) = sin ⁡ ( 4 5 ∘ ) = 0.707 \sin(\theta/2) = \sin(45^\circ) = 0.707 sin(θ/2)=sin(45)=0.707

2. 什么是四元素的双覆盖(Double Cover)?

四元素的“双覆盖”是指:对于同一个三维旋转,存在两个不同的四元素可以表示它。具体来说,如果 q q q 表示某个旋转,那么 − q -q q(即每个分量取负)表示的也是同一个旋转。这是因为四元素通过单位四元素的乘法对应到旋转群 S O ( 3 ) SO(3) SO(3)(三维旋转群),而这种对应是 2:1 的映射。

数学上:

  • 四元素属于 S U ( 2 ) SU(2) SU(2) 群(单位四元素的集合),而 S U ( 2 ) SU(2) SU(2) S O ( 3 ) SO(3) SO(3) 的映射是一个双覆盖。
  • 对于旋转角度 θ \theta θ 和轴 v ⃗ \vec{v} v ,可以用 q = cos ⁡ ( θ / 2 ) + sin ⁡ ( θ / 2 ) v ⃗ q = \cos(\theta/2) + \sin(\theta/2)\vec{v} q=cos(θ/2)+sin(θ/2)v 表示,但 q q q − q -q q 对应同一个旋转。
例子:
  • 旋转 180 度绕 z z z 轴:
    • q 1 = cos ⁡ ( 9 0 ∘ ) + sin ⁡ ( 9 0 ∘ ) k = 0 + 1 k q_1 = \cos(90^\circ) + \sin(90^\circ)k = 0 + 1k q1=cos(90)+sin(90)k=0+1k
    • q 2 = cos ⁡ ( 27 0 ∘ ) + sin ⁡ ( 27 0 ∘ ) k = 0 − 1 k = − q 1 q_2 = \cos(270^\circ) + \sin(270^\circ)k = 0 - 1k = -q_1 q2=cos(270)+sin(270)k=01k=q1
  • q 1 q_1 q1 q 2 q_2 q2 都表示绕 z z z 轴旋转 180 度。

3. 优势是什么?

四元素的优势:
  1. 紧凑性:只需 4 个数字表示旋转,而旋转矩阵需要 9 个。
  2. 无万向锁:避免了欧拉角在某些角度下的奇异性问题。
  3. 平滑插值:四元素可以用球面线性插值(SLERP)平滑过渡两个旋转,适合动画和运动规划。
  4. 计算效率:四元素的乘法和逆运算比矩阵更高效。
双覆盖的优势(或特性):
  1. 数学完备性:双覆盖反映了旋转群的拓扑性质( S O ( 3 ) SO(3) SO(3) 不是单连通的),在理论物理(如量子力学中的自旋)中有重要应用。
  2. 连续性:在路径规划中,双覆盖允许区分“旋转一圈”和“回到原位”,例如区分 360 度和 0 度的路径,这在某些应用(如机器人手臂控制)中很有用。

总结:

四元素是一个表示旋转的强大工具,双覆盖是它的一个数学特性,虽然在实际应用中我们通常只关心单一表示,但在理论和某些复杂场景下,双覆盖提供了额外的灵活性和精确性。比如在游戏引擎(如Unity或Unreal)中,四元素被广泛使用来确保旋转的平滑和稳定。

import numpy as np
import tkinter as tk
from tkinter import ttk
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg# 设置支持中文的字体并放大
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.size'] = 14# 四元素类
class Quaternion:def __init__(self, w, x, y, z):self.w = wself.x = xself.y = yself.z = zdef __str__(self):return f"{self.w:.2f} + {self.x:.2f}i + {self.y:.2f}j + {self.z:.2f}k"def multiply(self, other):w1, x1, y1, z1 = self.w, self.x, self.y, self.zw2, x2, y2, z2 = other.w, other.x, other.y, other.zw = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2return Quaternion(w, x, y, z)def conjugate(self):return Quaternion(self.w, -self.x, -self.y, -self.z)def rotate_vector(self, vector):p = Quaternion(0, vector[0], vector[1], vector[2])q_conj = self.conjugate()result = self.multiply(p).multiply(q_conj)return np.array([result.x, result.y, result.z])# 创建四元素
def create_quaternion(axis, angle_deg):angle_rad = np.radians(angle_deg)w = np.cos(angle_rad / 2)s = np.sin(angle_rad / 2)norm = np.sqrt(sum(a**2 for a in axis))if norm == 0:return Quaternion(1, 0, 0, 0)x, y, z = [a / norm * s for a in axis]return Quaternion(w, x, y, z)# 可视化工具类
class QuaternionVisualizer:def __init__(self, root):self.root = rootself.root.title("四元素可视化工具")self.root.geometry("900x700")# 设置全局字体self.font_large = ('SimHei', 14)# 初始向量self.initial_vector = np.array([1, 0, 0])self.rotated_vector = self.initial_vector.copy()self.current_q = Quaternion(1, 0, 0, 0)# 主框架self.frame = ttk.Frame(root, padding="20")self.frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))# 设置行列权重self.root.grid_rowconfigure(0, weight=1)self.root.grid_columnconfigure(0, weight=1)self.frame.grid_rowconfigure(0, weight=1)self.frame.grid_columnconfigure(2, weight=1)# 输入区域(左侧)self.input_frame = ttk.Frame(self.frame)self.input_frame.grid(row=0, column=0, sticky=(tk.W, tk.N, tk.S), padx=10)ttk.Label(self.input_frame, text="旋转轴 (x y z):", font=self.font_large).grid(row=0, column=0, sticky=tk.W, pady=10)self.axis_entry = ttk.Entry(self.input_frame, width=15, font=self.font_large)self.axis_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=10)self.axis_entry.insert(0, "0 0 1")ttk.Label(self.input_frame, text="旋转角度 (度):", font=self.font_large).grid(row=1, column=0, sticky=tk.W, pady=10)self.angle_entry = ttk.Entry(self.input_frame, width=15, font=self.font_large)self.angle_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=10)self.angle_entry.insert(0, "90")ttk.Button(self.input_frame, text="应用旋转", command=self.apply_rotation, style='Large.TButton').grid(row=2, column=0, columnspan=2, pady=10)ttk.Button(self.input_frame, text="重置", command=self.reset, style='Large.TButton').grid(row=3, column=0, columnspan=2, pady=10)# 四元素信息(中部)self.info_frame = ttk.Frame(self.frame)self.info_frame.grid(row=3, column=0, sticky=(tk.W, tk.N, tk.S), padx=10)self.q_label = ttk.Label(self.info_frame, text="当前四元素: 1.00 + 0.00i + 0.00j + 0.00k", font=self.font_large)self.q_label.grid(row=4, column=0, pady=10)self.double_cover_label = ttk.Label(self.info_frame, text="双覆盖版本: -1.00 + 0.00i + 0.00j + 0.00k", font=self.font_large)self.double_cover_label.grid(row=5, column=0, pady=10)self.explain_label = ttk.Label(self.info_frame, text="解释: 未旋转状态", font=self.font_large, wraplength=400)self.explain_label.grid(row=6, column=0, pady=10)# Matplotlib 嵌入(右侧)self.fig = plt.Figure(figsize=(7, 7))self.ax = self.fig.add_subplot(111, projection='3d')self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame)self.canvas.get_tk_widget().grid(row=0, column=2, sticky=(tk.W, tk.E, tk.N, tk.S))# 绑定窗口大小变化事件self.root.bind("<Configure>", self.on_resize)# 自定义按钮样式style = ttk.Style()style.configure('Large.TButton', font=self.font_large)# 初次绘制self.update_plot()self.last_width = self.root.winfo_width()self.last_height = self.root.winfo_height()def update_plot(self):self.ax.clear()self.ax.quiver(0, 0, 0, self.initial_vector[0], self.initial_vector[1], self.initial_vector[2], color='r', label='初始向量', linewidth=2)self.ax.quiver(0, 0, 0, self.rotated_vector[0], self.rotated_vector[1], self.rotated_vector[2], color='b', label='旋转后向量', linewidth=2)self.ax.set_xlim([-1.5, 1.5])self.ax.set_ylim([-1.5, 1.5])self.ax.set_zlim([-1.5, 1.5])self.ax.set_xlabel('X轴')self.ax.set_ylabel('Y轴')self.ax.set_zlabel('Z轴')self.ax.legend()self.canvas.draw()def on_resize(self, event):# 避免初始调整和重复绘制if event.widget == self.root:new_width = event.widthnew_height = event.heightif new_width != self.last_width or new_height != self.last_height:# 只调整图形大小,不重绘内容dpi = self.fig.dpiself.fig.set_size_inches((new_width * 0.5) / dpi, (new_height * 0.8) / dpi)self.canvas.draw()  # 只更新画布,不重新生成视图self.last_width = new_widthself.last_height = new_heightdef apply_rotation(self):try:axis = [float(x) for x in self.axis_entry.get().split()]if len(axis) != 3:raise ValueError("轴必须是3个数字")angle = float(self.angle_entry.get())except ValueError as e:tk.messagebox.showerror("输入错误", f"无效输入: {e}")returnq = create_quaternion(axis, angle)self.current_q = q.multiply(self.current_q)self.rotated_vector = self.current_q.rotate_vector(self.initial_vector)self.q_label.config(text=f"当前四元素: {self.current_q}")self.double_cover_label.config(text=f"双覆盖版本: {Quaternion(-self.current_q.w, -self.current_q.x, -self.current_q.y, -self.current_q.z)}")norm_axis = np.array(axis) / np.sqrt(sum(a**2 for a in axis))explain_text = (f"解释:\n"f"1. 四元素 = w + xi + yj + zk\n"f"   - w = cos(θ/2) = {self.current_q.w:.2f} (标量部分,表示旋转角度)\n"f"   - (x, y, z) = ({self.current_q.x:.2f}, {self.current_q.y:.2f}, {self.current_q.z:.2f}) (向量部分,表示轴)\n"f"2. 当前旋转轴: [{norm_axis[0]:.2f}, {norm_axis[1]:.2f}, {norm_axis[2]:.2f}]\n"f"3. 双覆盖: q 和 -q 表示相同旋转")self.explain_label.config(text=explain_text)self.update_plot()def reset(self):self.current_q = Quaternion(1, 0, 0, 0)self.rotated_vector = self.initial_vector.copy()self.axis_entry.delete(0, tk.END)self.axis_entry.insert(0, "0 0 1")self.angle_entry.delete(0, tk.END)self.angle_entry.insert(0, "90")self.q_label.config(text="当前四元素: 1.00 + 0.00i + 0.00j + 0.00k")self.double_cover_label.config(text="双覆盖版本: -1.00 + 0.00i + 0.00j + 0.00k")self.explain_label.config(text="解释: 未旋转状态")self.update_plot()# 启动工具
if __name__ == "__main__":root = tk.Tk()app = QuaternionVisualizer(root)root.mainloop()

在这里插入图片描述

版权声明:

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

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

热搜词