游戏引擎学习第275天:将旋转和剪切传递给渲染器
回顾并为今天的内容定下基调
我们认为在实现通用动画系统之前,先学习如何手写动画逻辑是非常有价值的。虽然加载和播放预设动画是合理的做法,尤其是在团队中有美术人员使用工具制作动画的情况下更是如此,但手动编写动画代码能让我们更深入理解动画系统的核心概念和细节,也更容易掌控一些动态和程序驱动的效果。将来即使使用外部制作的动画系统,在需要实时计算或程序生成动画的场景下,我们依然可以灵活介入和实现相应逻辑,而不会手足无措。
现在我们正处于动画系统开发的中段。我们目前做的是让角色实现跳跃移动的动画,基本实现已经完成,角色的身体部分已经具备了动画逻辑。但还有一个明显的问题需要解决:角色的头部目前是完全自由漂浮的,没有和身体相连。这种状态如果是设计成幽灵角色可能还说得过去,但我们实际上需要一个头部与身体连贯的角色。
因此,我们接下来的目标是让角色的身体能动态地拉伸,向头部延伸,即使头部短暂偏离身体的位置,也能保持一定程度的连接和协调。具体怎么实现这个效果还需要进一步探索,毕竟既要考虑实际的表现效果,也受到图形渲染方面的限制。
值得注意的是,在游戏开发中,不得不承认有时候响应性比美观性更重要。我们需要在视觉效果和操作反馈之间做出权衡,让角色动作看起来自然的同时,也保持游戏操作的流畅感和及时性。当前阶段就是围绕这个核心在进行探索和调整。
关于响应性与美学的一些讨论
在开发游戏时,控制响应性是首要任务,我们始终把“手感”放在第一位,而不是为了动画效果而牺牲操作体验。如果牺牲了操作响应速度去追求动画流畅,那通常是一个非常糟糕的决定。
早期的一些游戏,例如最初版本的《波斯王子》或《空手道冠军》,就是反面教材。这些游戏完全让动画来主导角色行为,比如按下跳跃键后,角色并不会立即跳,而是按照动画流程延迟一段时间再执行动作,结果就是操作手感变得极其迟钝、难以控制,玩起来体验很差。
所以在我们进行动画编程时,优先考虑的是玩家操作的即时性和流畅性。哪怕动画在某些情况下看起来有点奇怪、有点扭曲,只要不影响操作手感,这种视觉上的瑕疵是可以接受的。比如我们允许角色的头部在身体前面漂浮得有些远,看起来可能有点“拉伸”,但我们可以将其视作风格的一部分,而不是强行让头部不能移得太远。
接下来我们的主要目标仍然是让角色身体能够拉伸以连接头部,但在此之前还有一些小的问题需要处理。其中一个问题是当前的“回弹”机制不太合理——我们为头部添加了一个弹簧效果,使其在脱离身体后会被吸回身体。然而现在这个吸回的逻辑并不理想,它是吸向身体当前位置,而不是吸向身体将要去的地方。
理想的情况是:当玩家停止操作后,头部应该自然地朝向一个最终目标点靠拢,这个点不一定是当前身体位置,而是它的运动目标位置。简单来说,头部应该“预测”身体的位置,然后被吸向那个地方。我们之前加了个逻辑,头部只有在玩家没有主动输入方向时才会触发回弹,这一点基本没问题,但吸附的目标点还需调整。
因此我们要重构这一部分逻辑,让头部不是被吸向当前身体,而是吸向一个更接近其最终位置的点,这样可以使运动看起来更自然,避免“头部回弹到身体再猛然拉伸出去”的不协调感。
此外,我们可能今天无法立即实现身体拉伸的功能,因为还有一些系统层面的清理工作要先处理。这些细节虽然看起来是“小事”,但对整体感觉影响很大,处理好它们有助于最终动画系统的稳定和自然表现。
修改 game_world_mode.cpp
:引入 GetClosestTraversable
以使 HeroHead 和 HeroBody 都能寻找并吸附到最近的点
我们正在调整角色头部的回弹逻辑,让其更自然地回归到离自己最近的可行走区域点(traversable point),而不再只是简单地吸附到身体当前位置。为此,我们重构了一段查找逻辑,使得头部和身体都可以通过统一的方法查找最近的有效位置点。
具体操作如下:
-
进入 world_mode 模块:我们定位到控制头部位置更新的部分,把原本只为身体使用的“查找最近可行走点”逻辑提取出来,封装为一个独立函数
GetClosestTraversable
,这样可以供头部和身体复用。 -
封装查找逻辑函数:
- 函数输入为当前位置(
fromP
)和模拟区域(simRegion
)。 - 函数内部遍历模拟区域中的所有实体,寻找与当前点最近的、可行走的目标点。
- 输出为最近点(
closestP
),可作为头部或身体的新位置。 - 为了代码可扩展,准备未来加更多参数(比如返回距离、实体引用等),但目前只处理最基本的查找。
- 函数输入为当前位置(
-
调用新封装的函数:
- 在身体模块中,原本已有类似逻辑,现在替换为调用新函数,保持一致性。
- 在头部模块中,删除原本直接寻找身体位置并吸附的逻辑,改为同样调用该函数,查找当前头部附近的可行走点。
-
位置信息处理:
- 得到最近点后,我们将其作为头部的新位置,直接覆盖原有头部坐标,包括 XYZ(即使暂时可能不想修改 Z,也选择一并处理以保持一致性)。
- 这个点不再是身体的位置,而是通过空间搜索确定的独立位置,更适合头部当前的动态。
-
抽象与未来优化:
- 当前遍历是线性搜索,未来可能会添加空间加速结构(如四叉树、网格索引等)优化查询效率。
- 由于函数已经抽象好,后续替换查询实现时不需要修改主逻辑代码。
这样改完之后,我们的角色头部回弹逻辑就更加合理,不再强制吸附到身体,而是更加智能地吸附到最近的合法移动点。这种方式也为后续加入更多头身分离、动作表现多样化等功能打下了基础。
运行游戏,观察头部吸附效果
现在我们正在处理另一个问题:头部的朝向(facing direction)目前是根据实际移动方向来决定的,但这种方式存在一些明显的问题。例如,当我们操作角色移动时,如果因为惯性或回弹导致角色移动方向反转,头部也会随之反转,这在视觉上是不合理的,容易给人一种“抖动”或“翻转”的不稳定感。
为了让视觉表现更加稳定自然,我们希望头部的朝向不是基于“实际移动方向”,而是基于“上一次输入方向”,也就是我们最近一次按下方向键的方向。
当前问题的具体表现:
- 如果我们推动角色向右走,头部朝右;
- 但是如果因为运动过冲(overshoot)而稍微向左移动,头部立刻朝左;
- 实际上玩家并没有意图改变方向,只是物理运动造成了位移上的偏差;
- 所以这种做法会导致角色在快速移动和停止之间频繁翻转头部,显得非常突兀。
我们的改进思路:
- 记录“上一次的输入方向”,而不是依赖当前的速度或位移方向;
- 每次玩家主动输入移动指令时(如按下方向键),更新这个方向值;
- 渲染时,头部的朝向始终参考这个记录的方向,而不是当前移动方向;
- 这样即使由于物理模拟造成轻微反方向移动,头部也不会来回翻转;
- 提高了动画表现的稳定性和用户体验。
这个处理逻辑也能更贴合玩家的直觉,输入方向才是他们真实想表达的动作方向,视觉表现跟随这个方向显得更自然,也减少了因为“表现逻辑”干扰“操作感”的问题。接下来我们会在代码中查找并替换掉当前朝向判断方式,将其改为使用最近一次的输入方向记录。
修改 game_sim_region.cpp
:让头部保持朝向移动方向
我们遇到的一个问题是当前代码中头部朝向(facing direction)是由 MoveEntity
函数来设置的,但这种做法并不合理。因为 MoveEntity
是根据实体的实际移动方向来设置朝向的,而这并不能准确反映玩家的真实意图——例如,玩家可能只是因为惯性或者其他物理模拟效果让角色略微向后移动,并不代表他们真的想让角色转身,但朝向却被错误地改变了。
当前的问题点:
- 头部的朝向是在
MoveEntity
中设置的; - 该设置是依据角色的实际移动方向(velocity);
- 如果因为过冲、回弹等物理效果导致方向反转,朝向也会跟着改变;
- 这会导致头部在来回晃动时出现频繁翻转,带来不良的视觉体验;
- 更重要的是,它无法体现玩家真实的操作意图。
解决方案的思路:
我们决定放弃这种基于物理位移的方式,而是采用玩家输入方向作为头部朝向的依据。具体做法如下:
-
取消
MoveEntity
中控制朝向的逻辑
直接将该段设置朝向的代码清除掉(即“nerf out”); -
改为根据 DDP 设置朝向
DDP
是玩家输入控制的方向向量(比如手柄或键盘输入);- 它代表玩家希望角色移动的方向,而不是角色实际正在移动的方向;
- 这样,我们就能更准确地基于玩家的意图设置头部朝向;
-
设置判断逻辑
- 如果
DDP.x
有非零值,就更新朝向; - 否则保留当前朝向不变,这样可以防止在没有明确输入的情况下朝向抖动。
- 如果
优势总结:
- 准确表达玩家真实操作意图;
- 避免因物理模拟造成的视觉抖动或翻转;
- 视觉效果更加自然连贯;
- 提高游戏操作的响应性与流畅感。
这是一次重要的结构优化,我们将朝向控制的权力还给了输入控制系统,而不是物理系统,从而更好地服务于游戏的“可控性”与“手感”。
运行游戏,观察头部朝向的变化
目前的动画与控制系统已经基本达到了我们期望的目标,但还有一些细节需要继续打磨,以提升角色的表现力和玩家的操作体验。
当前效果总结:
-
头部移动与回弹效果:
我们已经实现了头部可以在角色运动时略微脱离身体,并通过一个“弹簧”机制(spring)将其拉回。这种效果带来了更自然的动态表现,尤其在角色快速变向或突然停止时有不错的惯性反馈感。 -
头部朝向控制:
已将头部朝向的控制权从实际物理运动改为玩家输入方向,这避免了来回晃动时因物理抖动导致的头部频繁翻转,使视觉效果更稳定、自然,也更贴合玩家操作的直觉。
当前需要改进的点:
-
头部与身体完全脱节
- 当前系统中,头部和身体几乎完全分离;
- 没有视觉或逻辑上的连接,像是两个独立实体;
- 下一步关键任务是实现“连接感”,例如通过拉伸、过渡、插值等手段,让它们看起来是连在一起的;
- 可以考虑引入一个动态伸缩的脖子模拟,或视觉上用插值方式让头部始终稍稍朝身体靠拢。
-
弹簧拉力(spring force)过于松弛
- 当前弹簧力度设置偏弱,导致头部回归位置的速度较慢;
- 为了让动作更紧凑、有力量感,我们决定将弹簧调得更“结实”,让头部更快地回到理想位置;
- 这会让角色表现看起来更果断和利落。
-
调整视觉反馈的一致性
- 当前如果移动过程中头部偏离身体较远,停止时的过渡还算自然;
- 但在极端偏移情况下仍可能显得略微突兀;
- 计划加入更流畅的插值机制,确保即使偏差较大,回弹和过渡依然平滑。
后续思路:
-
头部与身体的动态连接建模
不一定要真实模拟骨骼或软体连接,而是通过视觉手段来营造“连通感”;- 可以绘制一段“伸缩的脖子”;
- 或者用贝塞尔曲线绘制一段动态曲线连接点;
- 也可以通过身体向头部轻微插值调整,营造互相拉动的效果。
-
加入方向预测或前瞻机制
如果角色快速变向或跳跃,可以提前预判头部即将运动的趋势,提供更自然的预动作表现。 -
输入方向的进一步打磨
当前使用的是上一次输入方向,还可以改为“最近一次非零方向”或“输入持续时间加权方向”,让朝向判断更加稳定。
总体来说,我们的目标是让角色在不牺牲控制响应性的前提下,拥有流畅、自然又具有表现力的动画行为。我们不会强迫角色为动画“让步”,而是始终优先考虑玩家体验和操控手感,再通过技术手段让动画自然地跟随这种设计逻辑演化。
修改 game_world_mode.cpp
:关闭剑功能,准备实现侧移跳(strafe hop)
我们决定进一步调整角色朝向的控制方式,并移除之前用于实验的部分临时功能,使角色动作更加符合预期的设计目标。以下是具体思路与操作整理:
当前目标:支持“原地起跳”并改进角色朝向控制逻辑
移除无关代码:
之前加入的“挥剑”等相关逻辑,其实并不属于我们实际的游戏设计,只是临时为了测试动画效果做的实验。
这些功能现在被清除,以减少干扰和混淆,让我们能专注于角色核心行为的实现。
改进角色朝向控制方式:
旧方式问题:
- 原本角色的朝向是根据物理移动方向自动设定的;
- 在跳跃或快速移动后立即停止时,由于惯性导致的方向反转,会造成头部突然翻转;
- 这不仅不符合玩家预期,还可能破坏动画连贯性。
新方式:根据“最后一次攻击方向”决定朝向
- 改为以
D_sword
(最后一次挥剑/攻击的方向)作为角色面向的依据; - 即使玩家暂时没有移动,只要最近做过攻击,角色也会保持对应朝向;
- 这符合大多数动作类游戏的常规做法,朝向由主动行为而非惯性决定。
原地起跳支持:
- 为了支持不带移动的跳跃,我们希望角色能在原地起跳并保持攻击时的朝向;
- 这需要取消基于移动向量计算面向的旧逻辑,并优先使用最近一次攻击的方向信息;
- 这样角色在原地跳跃或空中调整时依然能保持正确的朝向,提升操作感和动画一致性。
下一步预览:
- 在控制逻辑中增加一个用于记录最后攻击方向的变量;
- 若当前没有攻击操作,就保持原有方向;
- 一旦进行攻击,立即更新面向方向;
- 后续攻击系统正式开发时将进一步完善该机制。
当前状态总结:
- 测试了“原地起跳”与“攻击朝向驱动”的结合;
- 清理掉了与设计目标无关的代码,减少系统复杂性;
- 正在逐步形成一个以“输入优先”、“行为驱动”的控制模型,动画、面向、物理行为等均围绕玩家输入而非动画逻辑进行构建。
通过这一步的调整,我们将角色控制逻辑进一步贴近实际游戏设计目标,同时保留系统的灵活性和扩展性,未来无论是加入攻击、连击、空中控制等内容都更容易集成。
运行游戏,尝试侧移跳动作
我们进一步测试了当前的角色控制与动作反馈,发现一些与“头部自动回正”相关的问题,这些细节虽小,但直接影响操作手感与玩家体验。以下是我们的详细总结:
当前控制手感问题分析
实际操作测试场景:
- 当角色以“平移”方式在一个水平网格上移动时,整体感觉顺畅;
- 比如沿着一排跳跃时,感觉自然流畅;
- 但一旦我们位于该网格的下方,并向上跳时,角色头部自动“吸附”回到中心线,这种 上拉回正的动作 让人感到不自然。
体验问题:
- 头部在跳跃方向变化后,会自动重新对齐(吸附)到目标格子中心;
- 这种回正行为在向上跳或偏离中心时特别明显;
- 当跳跃目标不是在一条直线上时,这种对齐动作会导致玩家感觉到“被拉动”、“被校正”,而非自然地完成动作;
- 用类比来说,就像模拟摇杆松手后指针强行回到中心,但我们期望的是“头部在空中时保持惯性位置”,而不是“强制吸附”。
问题本质:
- 当前系统中,角色头部在未输入方向时,采用了“回到身体位置”或“靠近网格中心”的策略;
- 这种机制原意是增强角色一致性与视觉稳定性;
- 但实际上,这种策略对跳跃状态并不友好,特别是在非水平跳跃轨迹上,视觉反馈与操作预期产生了冲突。
潜在改进方向:
1. 头部“回正”行为应具备条件触发:
-
不应该始终强制吸附;
-
在如下条件满足时才回正:
- 角色处于静止状态;
- 或者主动停止方向输入;
- 或者与身体位置距离过远(需自定义阈值);
-
否则保持原有惯性位置,避免突然被“拉回”。
2. 跳跃过程中的“自由模式”:
- 在空中跳跃期间,头部与身体位置的约束应适当放宽;
- 跳跃路径应优先跟随输入轨迹与速度,而非位置吸附;
- 保留一定弹性回拉感,但避免“吸附感”。
3. 区域性“朝向锁定”机制:
- 若在连续方向操作中(如多格跳跃),应保持头部方向与当前动作方向一致;
- 停止输入时才允许自动回正,模拟现实中动作惯性。
总结:
我们发现了当前系统在跳跃方向控制和视觉反馈方面的冲突,特别是在非水平移动时“吸附回正”的行为对玩家造成了干扰。为了解决这个问题,我们将调整自动对齐逻辑,使其更具条件性与物理感,从而提升操作自由度与自然性。
下一步将尝试将“头部回正”逻辑限制在明确条件下触发,并对跳跃动作引入“自由跟随”机制,优化整体运动体验。
修改 game_world_mode.cpp
:让头部不再自动居中
我们重新审视了“头部回正”这一机制,发现之前删除的那段代码或许仍有其存在的必要,尤其是在处理没有任何方向输入时的角色行为。以下是这部分逻辑的详细总结:
回正机制的再评估
原本的假设:
- 起初我们认为“自动回正”的代码可以被移除,因为它对操作感觉产生干扰;
- 实际上,在某些情况下——例如没有输入操作时——这种回正是必要的,用来确保角色的姿态和方向状态一致。
当前的策略修正:
- 不再完全删除回正逻辑;
- 而是根据是否存在输入来决定是否启用回正;
- 我们引入一个判断变量
shouldApply
来控制是否施加回正行为。
判断逻辑说明:
shouldApply = (Length(DDP) < 0.1);
解读:
DDP
表示当前控制输入的方向向量(即玩家当前是否正在推动摇杆或方向键);Length(DDP)
表示这个方向向量的大小(也就是玩家输入的“力度”);- 当这个值小于某个很小的阈值(例如 0.1)时,说明玩家基本没有输入动作;
- 此时我们将
shouldApply
设为true
,允许执行“回正”操作; - 否则保持当前惯性状态,不进行任何吸附或回位,避免打断操作体验。
实际效果:
- 在角色处于非移动状态时,头部会以弹簧方式自然回到中心位置或靠近身体位置;
- 在玩家持续推动方向时,头部将维持惯性方向,不会被回拉;
- 这样既保留了自然反馈,也避免了强行吸附带来的违和感;
- 实现了**“按需回正”**的机制,使得角色控制更加流畅。
技术上的简洁实现:
- 判断输入矢量的长度是否接近零,是一种计算量小、结果清晰的判断方式;
- 无需复杂状态切换逻辑,只需在关键代码处包裹
if (shouldApply)
即可生效; - 后续也可拓展为仅在特定状态(如落地、静止、受击)下再执行回正。
小结:
我们通过“输入矢量长度判断”的方式,优化了头部回正机制,使其在玩家没有方向输入时才触发,避免了不必要的吸附行为,提升了整体控制的灵敏度与自然感。这种设计既保留了视觉一致性,又尊重了玩家的输入意图,是当前系统中较为合理的改进方案。
运行游戏,测试新移动效果
我们确实感觉这样做更好一些。虽然有点不太规范,但我们还是决定这样做。我们可以考虑的一个解决方式是对其进行一定的调整,从而让整体结构看起来更自然,同时也保持操作的简洁性和实用性。虽然这种方式有些“奇怪”或“临时”,但从体验上来说,它确实带来了改进,所以我们更倾向于保留这种做法。我们理解它可能不符合常规流程,但权衡之后还是选择了这样更有效率的路径。
修改 game_world_mode.cpp
:使用更弱的弹簧让头部回中
我们打算根据弹簧的刚度来进行调整。我们的思路是,保留一段代码用来判断是否需要施加任何力,而另一段代码则用来决定施加多大的力,也就是系数的设定。这部分使用的是一个 real32 值,它相当于是我们在弹簧位置上的一个系数。
具体来说,如果当前没有任何推动作用,那么我们就将弹簧设定为一个非常强硬的状态,也就是非常高的刚度;相反,如果有某种推动方向,但这个方向并不是我们希望进行修正的方向——比如某个特定轴上的情况——那么我们就会施加一个相对较弱的力,温和地将其引导回理想位置。
这个弱力的设计目的在于足够细微,从而让使用者在体验中几乎察觉不到这种引导,或者即便察觉也不会感到不适。通过这样的处理方式,我们希望能实现既自然又不具干扰性的回正机制。
运行游戏,尝试新的居中效果
所以整体感觉上会稍微更有一些“深度”,有点像是沿着对角线的方式把你拉回来。这个回正的方式不是直接、垂直地把你推回原位,而是带有一定角度、稍微斜着的那种感觉,就像是从一个更柔和、更间接的方向进行干预。
这种处理让回到目标位置的过程看起来更自然,也更流畅,不会突然产生明显的纠正动作,而是像一种顺势而为的引导。不过也正因为这样,它带来的反馈感会比较模糊,有一种说不上来的微妙变化。目前我们对这种方式的感受还不太确定,内心有点摇摆,不完全确定这样的实现是否真正是理想的,有些犹豫。
修改 game_world_mode.cpp
:再次让头部不再自动居中
整体上还是感觉不太理想。最舒服的体验可能反而是不去做这些额外的干预。我们有点倾向于不去强行引导回正,或者说,这种引导可能只适合在特定情况下才发生,比如说在“着陆”的瞬间,才适合进行修正。
现在的处理方式似乎是把任何推动都当作需要修正的信号,导致体验上不够自然。这可能过于敏感,反而让系统显得拘谨、不流畅。我们开始意识到,也许不应该为所有推力都设定相同的响应方式。
从整体感觉上看,它更像是想要一种“自由漂浮”的状态,不被过多限制,让物体在空间中自由运动,只有在真正需要的时候才施加限制或引导。虽然我们知道这些设置都可以随时调整回来,但从目前的反馈来看,似乎朝着更加开放、自由的方向发展,会带来更舒服的体验。
运行游戏,测试移动效果
我们决定暂时就保持现在的状态。这个问题最终还是要等到实际游玩时再根据手感来判断。毕竟,从整体设计角度来看,这个改动不会带来很大的影响,但游戏的调校必须以手感为优先,这最终是由负责这个部分的人来决定的。
在当前的状态下,不去强行施加约束会让整体感觉更自然一些,所以我们决定就先这样保留。虽然可能看起来有点奇怪,但这确实是我们更倾向的处理方式。
通常在任何以网格为基础的系统中,为了清晰性,角色完全对齐网格是最理想的状态,但在游戏体验方面,这种硬性的限制往往会带来负面效果,特别是在动作类游戏中。如果角色被强行锁定在格子上,移动起来的手感通常会变得呆板、生硬。
因此我们需要格外谨慎,避免因为网格系统的存在而破坏了角色控制的自由度。网格的存在应该是“自然而然”的,玩家能察觉它存在但不会因此受到限制。这也是为什么我们在设计上将身体和头部分开处理,就是为了保证这种结构能正常运行并带来好的体验。
目前这种处理方式的反馈越来越好,感觉也逐渐趋于理想。接下来,我们要开始解决最后一个关键部分,也就是让身体和头部之间的连接逻辑完善起来。一旦完成这部分,就可以回头去处理渲染器方面的问题。
具体来说,我们要整理渲染器的内容,让一切的排序逻辑变得正确。比如角色图像的绘制顺序现在是反的,这类问题都需要修复。此外还要稍微整理一下英雄角色的位图,使其显示得更符合预期。
为了实现这些改动,我们需要开放渲染器的一些能力。目前渲染器具备缩放、剪切、旋转等功能,但在实际使用中我们还没有真的在位图调用中使用过这些功能。
所以我们接下来的任务,是打开渲染器的相关代码,并为其增加参数,使我们在设置一个位图时,可以附带地指定是否需要缩放、旋转或者剪切等操作。要让这些变成渲染流程的一部分,这样才能实现身体和头部分别渲染,并具有正确的视觉表现。
我们现在就要开始解决这个问题,看看具体该怎么做。
修改 game_render_group.h
:为 render_entry_bitmap
添加 X 轴和 Y 轴
进入游戏渲染组后,发现当前使用的是 render_entry_rectangle
类型的渲染条目。这种条目可以进行缩放操作,但不支持旋转和剪切,也没有轴向的定义,只有尺寸信息(dimension)。为了增强灵活性,计划增加一种新的渲染条目类型,比如 render_entry_flexible_bitmap
,用于更灵活地处理位图渲染。
与现有的渲染方式不同,新的条目除了包含颜色、位置(P点)和尺寸信息外,我们不再简单地指定尺寸,而是通过 x 轴和 y 轴来描述图像的方向和伸展方式。这两个方向向量将从点 P(位置)出发,用于控制图像的方向和形状。
为了实现这一点,需要在渲染器中新增一个 push_bitmap
的调用接口,使得每次渲染时都可以带上这些额外的方向信息。虽然可以强制每次都指定这些方向信息,但考虑到可能有些图像并不需要旋转或剪切,可能不需要每次都附带这些信息。为了提升渲染效率,可以保留不使用旋转和剪切的渲染方式,但实际上,判断是否需要这些额外变换并不复杂,只需检测 x 轴和 y 轴的状态就能判断。
最终决定,虽然有可能统一使用新的方式,但为了实现这一目标,我们并不会改变当前渲染的最终效果,而是通过调整底层的实现,将方向性的信息作为编码的一部分,逐步调整渲染过程。通过这样处理,我们仍然渲染相同的内容,但现在每个图像的渲染信息中不仅仅是尺寸,还有方向性的数据。
接下来,我们将进行必要的调整,以便支持这些新的方向性定义,同时确保当前的渲染效果不受影响。
修改 game_opengl.cpp
:让 OpenGLRenderCommands
支持 render_entry_bitmap
首先从 OpenGL 渲染器开始,因为这是当前正在使用的渲染方法。之后会切换到软件渲染器,确保两者都能够正常工作。由于软件光栅化器已经能够处理这种情况,转换起来相对简单,因此不需要重新编写渲染代码,只需做一些适配即可。
现在,我们可以看到当前的渲染方式是通过调用 OpenGL 的 rectangle
函数来绘制矩形。但是,矩形的渲染不符合我们现在的需求,因为我们不再需要只绘制矩形。所以需要做一些修改,让渲染过程不再局限于矩形。
首先,需要插入一些新的代码,来支持非矩形的渲染方式。接下来,修改 maxP
和 minP
,它们已经准备好用于流动处理。如果我们不做任何其他操作,只要确保这些变量正确流动,就能实现预期的效果。
为了进一步简化,渲染时还可以传递颜色信息,通过传递一个颜色指针来加载颜色。这样,我们可以删除 OpenGL rectangle
的调用,直接使用新方式进行渲染。
接下来,需要检查这个修改是否有效,并确保它不会破坏当前的渲染效果。如果一切正常,渲染的结果应该和之前一样。
由于这些修改涉及渲染代码,所以需要重新启动应用程序,因为这部分代码并不在可以动态加载的区域中。重新启动后,我们就能看到这些修改是否生效。如果一切顺利,屏幕上显示的内容应该和之前一致,这也正是我们希望的效果。
黑板讲解:处理平行四边形
接下来需要做的工作是让代码能够支持拉伸版本的位图。为了实现这一点,不再使用最小和最大坐标来定义矩形的边界,因为位图可能会经过剪切、旋转或缩放。原本的两个边界点(最小点和最大点)已经不够用了,我们需要四个独立的点来描述这些变换后的形状。
如果我们只绘制一个矩形且与屏幕对齐,那么定义矩形只需最小坐标 (minP) 和最大坐标 (maxP) 就足够了。这两个点可以直接指定矩形的边界,简化了处理过程。最小坐标和最大坐标实际上就是矩形四个边界平面的交点,确定了矩形的位置和大小。
然而,问题出现当我们需要绘制一个经过旋转、缩放或剪切的矩形时。在这种情况下,无法再简单地通过最小和最大坐标来定义矩形。为了支持这种情况,我们需要四个点来定义矩形的四个角。举个例子,假设我们有一个图像进行剪切、旋转或缩放后,变成了一个非矩形的形状,这时就需要通过四个点来表示这个图像的变换后的位置和形状。
为了生成这四个点,可以通过已有的中心点 § 来计算。具体方法是:首先,通过已知的最小和最大坐标来计算出四个新的点。这些点分别通过以下方式获得:
- 从中心点减去最小坐标,得到第一个点;
- 将中心点加上最大坐标减去最小坐标得到第二个点;
- 将这两个坐标的差异加起来得到第三个点;
- 最后,将中心点加上最小坐标并减去最大坐标得到第四个点。
通过这种方式,能够从给定的两个点生成四个独立的点,这样就可以在渲染时使用这些点来描绘经过变换的矩形,适应旋转、缩放和剪切的需求。
因此,关键的步骤是通过已有的坐标计算生成四个角点,并使用这些点来定义渲染区域,而不是简单的使用最小和最大坐标。
修改 game_opengl.cpp
:计算四个顶点
接下来需要做的是生成四个关键点,这些点表示经过旋转、缩放和剪切的矩形的四个角。为了实现这一点,首先,我们需要根据已有的最小坐标 (minP) 和最大坐标 (maxP) 来计算这些点的位置。这些点将根据已知的方向和大小来生成,而不是像以前那样通过简单的最小和最大坐标来定义矩形的边界。
具体步骤如下:
-
**确定最小和最大坐标:**首先,我们定义最小和最大坐标点,包括
minX, minY
和maxX, maxY
,这两个点会被用于计算矩形的四个角。为了确保它们能够适应旋转和缩放,我们需要同时处理x
和y
轴的信息。 -
**计算四个角点:**通过给定的
P
(即中心点)和这两个点的信息,我们可以计算出四个角点:minX, minY
对应的点;maxX, minY
对应的点;minX, maxY
对应的点;maxX, maxY
对应的点。
-
**计算方法:**通过在原点
P
上进行加减运算,可以得到这四个角点的位置。具体来说:minX
通过减去x
轴的值来计算;maxX
通过加上x
轴的值来计算;minY
通过减去y
轴的值来计算;maxY
通过加上y
轴的值来计算。
这些计算会生成四个角点,这四个角点定义了一个任意大小和形状的平行四边形。通过这种方法,可以处理旋转、缩放和剪切的情况,使得渲染更加灵活。
-
**更新渲染器:**这些计算得到的点需要在渲染器中进行记录,以便渲染时能够正确地使用这些信息进行渲染。渲染器不再只使用最小和最大坐标,而是使用这四个计算出的点来绘制可能已经经过变换的位图。
-
**优化渲染:**在这个过程中,我们不再需要显式地乘以大小,因为这些点已经根据实际的尺寸进行了缩放。可以简化渲染过程,减少不必要的计算,并使得图形的变换更加准确。
最终,通过这套方法,我们能够确保渲染器能够处理旋转、缩放和剪切后的任意形状,渲染出符合要求的图像,并且确保所有的变换正确地应用到位图上。
修改 game_render.cpp
和 game_render_group.cpp
:让 DrawRectangleQuickly
和 PushBitmap
支持 render_entry_bitmap
接下来,任务主要是将修改后的渲染器与现有的软件渲染器对接。当切换回到软件渲染器时,可以看到原来在渲染位图时调用的是绘制矩形的功能。为了支持新的变换功能,需要做的主要工作是将新的计算传递给渲染器,而不是修改渲染器的代码本身。
具体步骤如下:
-
**支持现有的
push bitmap
调用:**在渲染过程中,不需要对渲染器代码进行复杂的修改。现在,所有需要做的是将新的坐标信息传递给渲染器。我们已经设定好如何计算这些点,因此,渲染器可以处理这些信息而不需要进一步的改变。 -
**调整坐标传递:**为了支持新功能,只需要将
size
作为一个v2
类型来传递。这样,我们可以确定x
轴和y
轴的值,因为它们分别对应于sizeX
和sizeY
,这两个值会作为x
和y
组件传递给渲染器。 -
**清理代码:**在代码中,有一些拼写错误或者遗漏的地方,需要清理一下,确保传递正确的参数和调用渲染函数。主要是修改一些变量名和确保调用的参数符合要求。
-
**测试:**做完这些修改后,可以运行程序测试,确保渲染效果正确,并且所有变换(如旋转、缩放、剪切)能够按照预期应用到位图上。
总结起来,核心任务是将已经修改过的坐标信息正确地传递到渲染器,而不需要对渲染器本身做太多修改。一旦调整了传递参数并修正了代码中的小错误,渲染器就能处理新的变换方式,确保最终渲染效果符合预期。
运行游戏,发现渲染出了问题
我们在这段过程中尝试运行修改后的渲染代码,希望能在屏幕上看到和原来一样的渲染效果,前提是没有出错。但显然结果并不如预期,出现了明显错误,情况比想象中更严重,问题看起来很明显,甚至可以说有些“壮观”。
意识到这一点后,我们回过头检查问题的来源,并很快想到可能和 render_group
有关。很可能是在设置或调用 render_group
的过程中出现了疏漏或配置错误,导致渲染效果完全偏离预期。
总结来说,我们尝试的逻辑是合理的,但在实现过程中确实出现了某些具体错误,这种错误虽然不希望发生,但也有助于我们更准确地定位系统中尚未完善的部分,从而继续进行调试与修复。
修改 game_open.cpp
:修正顶点计算
我们意识到在处理渲染坐标时,原本对坐标点进行了一些不必要的减法操作,尤其是在处理左下角(min-min)的顶点时。我们本来是先从中心点出发,再减去 x 轴和 y 轴方向上的向量,以得到这个角的位置,但现在我们发现这种减法其实是不需要的。
因此我们决定不再对 min-min 点进行坐标减法操作,而是直接使用当前的基准点和轴向信息来构造出各个顶点的位置。这也意味着我们不需要再像之前那样显式地进行“加上或减去向量”的操作,而是可以简化为更直观的坐标构建逻辑。
这个修正帮助我们清理了之前的多余代码,并重新构建了一个更简洁、准确的顶点生成方式,以确保图形在经过旋转、缩放、变形等变换后,仍能正确渲染出想要的形状。我们在这里也注意到,之前误删了一些有用的代码,但最终我们将逻辑重新理顺,恢复并优化了结构。整体而言,这是在图形渲染流程中一个小而关键的调整步骤。
黑板讲解:我们是如何计算这些点的
我们之前在处理顶点坐标时,错误地采用了“从中心点出发再减去一半尺寸”的做法,这实际上是建立在“尺寸是半边长”这种错误的心智模型之上。但我们现在明确了,我们的尺寸参数其实已经是完整的宽度和高度,而不是半尺寸。
因此,原先的减法操作其实导致了图形尺寸被错误地放大了一倍,因为我们本来就已经有了完整尺寸,还在代码中多减了一次,等于是进行了“加加”操作,逻辑上重复了一次变换。
现在我们认识到,只需要从参考点 P(左下角)开始,依次加上 x 轴方向的向量和 y 轴方向的向量即可,无需再进行减法操作。这也让坐标构建的逻辑更加直观清晰,简化了之前的混乱计算。
我们修正了这个问题,回到了正确的计算方式,并重新校准了对渲染流程的理解。最终我们得出了准确的顶点构造方法,并解决了由于错误心智模型带来的图像变形和缩放问题。现在整体渲染逻辑已经回归正常,效果也符合预期。
运行游戏,看到渲染正常了
现在我们已经成功实现了对任意缩放、错切(shear)以及旋转的图像进行渲染的能力,这是我们渲染系统所必须具备的功能。
在此前的基础上,我们对渲染器进行了调整,从只支持规则矩形绘制,扩展到了支持任意平行四边形形状的位图渲染。通过引入 x 轴和 y 轴两个方向向量,并以某个起始点 P 为基准点,我们能够计算出组成图像四个顶点的具体位置。这样,我们就能构建出一个可以随意旋转和变形的图像框架。
具体来说,现在渲染器可以接收:
- 一个起始点 P(通常是左下角)
- 两个方向向量,x 轴方向与 y 轴方向,这两个向量包含了缩放、旋转以及错切的信息
然后,利用这三个参数,计算出其他三个顶点的坐标,拼出一个具有任意变换属性的四边形用于贴图渲染。整个过程不再依赖固定的 min/max 坐标形式,而是以向量为基础,灵活性更强,通用性更好。
这使得我们之后在渲染任何复杂角色、物体、动画时,都可以自由应用各种几何变换,而无需更改渲染管线的核心逻辑,只需构造合适的方向向量即可。
下一步,我们只需要把这个机制广泛应用到实际的渲染调用中,并确保所有推入渲染器的图像数据正确使用这套新的参数系统,就可以实现全局支持变换的
渲染流程了。
修改 game_render_group.cpp
:考虑如何更好地指定轴向
现在我们进入了渲染调用逻辑的最后整理阶段,重点在于如何高效、清晰地将我们前面准备好的位图信息(包含旋转、缩放、错切等变换)通过接口传递到渲染器。大部分工作其实就是围绕如何优雅地组织这段调用代码。
目前已经有多个 PushBitmap
调用方式,但它们参数较多,调用起来较繁琐。为了解决这个问题,我们希望能够引入一种更简洁的方式,例如通过传入两个向量:xAxis
和 yAxis
,这两个向量分别表示位图在屏幕上的 x 轴和 y 轴方向。我们可以简单地将原有的尺寸 Size.x
和 Size.y
分别乘以这两个向量,构造出带变换的四边形。
不过有一个潜在问题需要考虑:投影缩放是非线性的。如果直接在外部对向量乘以尺寸再传进去,可能导致投影失真。但我们进一步审视代码后发现,我们已经将这种非线性处理封装到了 GetRenderEntityBasis
的内部:
- 首先,
GetRenderEntityBasis
会计算出一个基准缩放值MetersToPixels
- 接着会用一个投影矩阵对传入的原始向量做投影(特别是 z 分量),再将其 dot 后进行缩放
- 所以非线性变换实际上被包含进了
XY
投影的过程中 - 这样做的好处是,我们可以安全地传入“单位长度”的方向向量,然后由
height
决定最终缩放值,并自动保留所有透视和投影信息
由此我们得出一个可行的方案:
- 仍然保留
height
作为输入参数,由它控制缩放比例 - 新增
XAxis
和YAxis
两个单位向量作为方向参数 - 内部逻辑使用这两个向量和
height
一起生成最终投影用的 x/y 基向量 - 如果用户希望保持纵横比,还可以只传
YAxis
,然后根据图像尺寸自动生成XAxis
这种做法的好处是:
- 保持 API 的清晰和结构化,便于内联优化
- 便于支持精确控制的旋转、缩放、错切等几何操作
- 同时保留了一种“简单调用方式”(只传 height)的接口形式
最后我们将会根据实际的位图使用场景,进一步丰富这个接口的重载版本,提供更灵活的调用方式。例如:
- 简单图标渲染:只传位置和尺寸
- 动态特效渲染:传入自定义轴向,实现旋转/缩放
- 动态投影精度控制:结合非线性投影和基准长度控制大小
总体上,通过把向量传入 PushBitmap
接口,我们完成了渲染管线中“变换感知”的最后一环,确保从上层逻辑到底层渲染的一致性和灵活性。接下来要做的就是将这些调用方式在实际游戏渲染中应用起来。
黑板讲解:旋转身体的 Y 轴指向头部
当前我们正在处理角色(比如主角)渲染时的对齐、旋转和变形问题。这个角色立在一个类似梯形的平台上,角色本身也是一个类似梯形的图形。在角色内部,我们定义了一个“头部”点,这个点可能会发生位置变化(比如转头、弯腰等动作)。我们的问题是:如何根据头部的新位置,修正身体的姿态,使其在视觉上更自然?
我们目前的变换方式使得身体部分产生了偏移,为了让身体正确“指向”头部,需要做一些修正。这个修正有两种思路:
- 错切(Shear):通过斜切整个图形,使其从底部保持固定,而顶部偏移到目标位置。这样变换更像是“扭转”身体;
- 旋转+缩放:保持身体轮廓为矩形,通过整体旋转再适当缩放,达到目标头部位置。这种更类似“侧身”或“倾斜”动作。
选择哪种方式更合适,取决于我们想要实现的视觉效果。
当前位图的渲染状态
目前我们对位图的控制存在一定局限:
- 位图的尺寸(宽高)来源于外部美术资源文件;
- 我们只知道实际渲染时的尺寸高度,宽度往往是由原始比例决定的;
- 这意味着我们在上层代码中,其实并不直接知道最终渲染时的确切宽度;
因此,我们倾向于继续使用“高度 + Y轴向量”的方案来控制位图的缩放或弯曲,只要我们能够控制好 yAxis
,那么视觉上的变换就会比较自然。
关于对齐点(Pivot)的复杂性
我们现在还面临一个更复杂的问题:位图的对齐点(Pivot)不在我们真正需要的位置。具体来说:
- 当前对齐点是美术打包资源时设定的,它们一般设置在角色脚底,即接地位置;
- 但我们现在要对角色进行身体-头部分离的独立动画,所以我们更希望这个对齐点在“身体连接头部的位置”(比如肩膀、颈部或躯干顶部);
- 如果我们只是直接在原始基础上做变换,渲染的效果会错位;
- 因此,我们需要将整个位图的位置进行反向偏移,让我们变换的参考点对齐我们需要的逻辑“连接点”;
- 否则,角色的身体会随头部的动作“漂移”,而不是保持底部固定;
当前对齐代码逻辑分析
我们使用的是 GetBitmapDim()
这个函数,它内部包含了 AlignPercentage * Hadamard(width, height)
的代码逻辑,也就是说对齐点是基于资源打包时美术定义的百分比。我们推测,这些对齐点是默认在“接地”位置,这在分离部件动画中并不合适。
我们可以通过以下方式解决这个问题:
- 重定义对齐点:在资源打包阶段,重新指定更合理的对齐点(比如躯干上部),用于分段动画时;
- 运行时覆盖对齐点:在调用
PushBitmap
时,显式设置对齐参数,覆盖资源默认的对齐百分比; - 反向计算偏移量:在上层逻辑中手动添加平移矢量,使变换后的位置保持视觉对齐;
后续处理方向
这项工作需要继续进行几步来完善:
- 明确每个角色部件(头、身体、腿等)所用的对齐点;
- 更新或重写资源打包时的对齐配置;
- 修改上层动画系统,使它能够合理传入正确的对齐位置和变换向量;
- 验证当前变换是否需要 shear 或 rotation,决定最终视觉实现逻辑;
- 最后,在绘制时确保 pivot 反向修正完成,使动画自然、平稳、可控。
这个过程会比较繁琐,但最终能让我们实现完全可控、动画精确、视觉自然的分段角色动画系统。
修改 game_render_group.cpp
:让 PushBitmap
和 GetBitmapDim
接收 X 轴和 Y 轴
我们现在决定采用更灵活的方式来传递位图渲染所需的变换信息:直接传入 X 和 Y 轴向量(xAxis
和 yAxis
),并在内部根据这些向量与尺寸信息相乘,从而计算最终渲染的变换矩阵。这种方式使我们可以实现旋转、缩放、错切等更复杂的视觉变换。
关键变更点总结如下:
保留原有逻辑,但支持扩展
我们保留了先前的逻辑结构,但加入了对 xAxis
和 yAxis
的支持。通过:
scaledXAxis = size.x * xAxis;
scaledYAxis = size.y * yAxis;
这样的方式将尺寸与方向绑定,允许直接控制图形的空间方向。
保持兼容性
我们修改了 GetBitmapDim
函数的参数,使其支持 xAxis
和 yAxis
作为输入,但保留了默认值,因此现有的所有调用方式仍然有效,无需重构外部接口,保证了兼容性。
正确计算偏移量(Offset)
由于位图可能被旋转或错切,所以我们不能再假设偏移(offset
)只在平面直角坐标系下处理。我们需要将对齐百分比(AlignPercentage
)应用在当前的 xAxis
和 yAxis
上:
offsetXY = AlignPercentage.x * xAxis + AlignPercentage.y * yAxis;
而不是:
offset = AlignPercentage * size; // 原始的二维偏移方式
这样处理之后,无论位图被如何旋转或错切,对齐点依然能够正确地控制最终图像位置。
Z 轴保持不变
因为我们目前只处理 XY 平面的变换,Z 坐标保持不变,仅从原始偏移中取出:
dimP.z = offset.z;
这使得我们可以在渲染层保留高度信息(比如图像的叠放层次),而不干扰平面变换逻辑。
验证无副作用
我们检查了 GetBitmapDim
的使用范围,确认它仅用于文本渲染和调试块显示系统,都是非关键渲染路径,因此本次修改影响面小、风险低。初步运行后也验证没有带来可见的变化,说明改动保持了现有功能的稳定性。
小结:
通过引入 xAxis
和 yAxis
向量,我们获得了更高的变换自由度,实现了更加通用的位图变换接口。这个接口允许我们在保留兼容性的前提下,进行精确的空间控制,是实现复杂角色动画、斜切、倾斜、旋转等视觉效果的关键基础。同时,对齐点的处理也更合理地支持了这些变换,从而避免图像错位或浮动等问题。后续只需在调用处传入合适的变换轴向量,即可实现丰富的动态表现。
修改 game_world_mode.cpp
:使身体的 Y 轴朝向头部
我们现在的目标是实现角色身体部分的动态拉伸效果,也就是根据头部和身体之间的距离变化,使身体能够随头部位置的移动而产生相应的形变,模拟一种自然的拉伸或扭曲。整个过程分为几个关键步骤,现已初步实现了基础框架,具体细节如下:
1. 计算头部与身体之间的相对位移
我们首先获取了头部相对身体的位置差值 headDelta
,这是一个三维向量(v3),它表示头部位置相对于身体位置的位移,用于后续计算形变。
2. 修改渲染逻辑以支持不同的变换轴向量
接下来,我们调整了渲染代码,使身体和披风使用独立的 X/Y 轴向量进行绘制,不再使用默认方向。这意味着我们现在可以在渲染前指定任意变换方向,用于控制图像的拉伸、旋转、错切等。
我们确保所有 PushBitmap
的调用都传入了 xAxis
和 yAxis
参数,并在渲染流程中保持正确传递和计算。
3. 将变换轴存入实体结构中以供更新使用
为了让逻辑与渲染解耦,并能在更新阶段控制角色外形变换,我们在 SimEntity
结构体中新增了两个字段:
v3XAxis;
v3YAxis;
这些向量用于存储每个实体当前的 X/Y 渲染轴,默认设置为标准轴(单位向量),后续在更新逻辑中根据实际情况进行修改。
4. 拉伸逻辑的初步实现
我们尝试了一种简单方式来模拟拉伸:保持 x 轴不变,仅对 y 轴进行修改,使其受到头部与身体之间距离的影响。基本方法是:
entity.yAxis = defaultYAxis + (stretchFactor * headDelta);
这样做虽然不能完全精准地模拟正确的物理变换(因为还未考虑旋转基点位置等问题),但作为第一步测试已经能够展示出基本效果。
5. 调试与验证
初次运行未见效果,经排查发现是传递参数时漏掉了某些路径。在补充好参数传递链后,测试发现拉伸效果生效,确认变换轴向量能够正确影响图像的变形。
我们也尝试将 y 轴拉伸两倍以验证基础逻辑是否正确,测试结果表明渲染变形工作正常。
6. 后续考虑:枢轴点(pivot)的问题
当前的实现中,图像的枢轴点(即图像的旋转或形变基准点)仍使用的是打包资源时的默认对齐点,该对齐点可能是基于“地面”的,而非身体与头部的连接部位。
这导致拉伸时形变的参考位置偏移,不符合我们期望。因此后续需要在资源打包阶段修正这些对齐点,确保图像的枢轴点位于角色正确的身体连接部位(如颈部)。
当前阶段成果总结
- 实现了可控的
xAxis
和yAxis
渲染系统; - 在实体结构中引入变换向量,支持逻辑控制形变;
- 成功使角色身体拉伸受头部位置影响,变形生效;
- 验证了向量变换渲染路径在图形层面无误;
- 已识别并确认需后续处理枢轴点不匹配问题。
整体流程较为复杂,但目前已经建立了基础框架,后续可以进一步完善动画效果、处理旋转错切等更复杂变换,并修复对齐点误差。现在这个系统已经具备较强的图像空间控制能力,为角色动态表现打下了坚实基础。
运行游戏,观察拉伸效果
我们目前已经成功实现了角色身体部分的拉伸效果,渲染系统层面已经可以支持动态变换,包括根据头部相对位置动态拉伸身体。然而,目前的拉伸效果虽然在技术上可行,但从视觉效果来看仍然不够理想,具体问题和现状如下:
当前系统表现
- 渲染器已经能够根据传入的
xAxis
和yAxis
向量对图像进行拉伸和变形。 - 身体部分的
yAxis
向量现在可以动态地根据头部与身体的相对距离进行偏移,从而实现一个视觉上的“身体伸展”。 - 整体效果相较于之前是有所改进的,角色不再是完全静态不变的模型。
存在问题
-
拉伸效果生硬、不自然
由于当前变形只是简单地对yAxis
向量做了线性调整,缺乏更高阶的插值或动画过渡,因此画面显得不够顺滑,视觉上比较“硬”。 -
艺术资源未对齐
目前使用的图像资源其枢轴点(即图像连接点)没有与逻辑动画系统对齐。例如,身体与头部的连接点并不是我们进行拉伸操作所基准的点,导致视觉错位。 -
缺乏对扭曲、错切等更复杂变形的支持
现阶段只实现了基本的拉伸操作,如果需要更自然或更复杂的动态表现,还需引入旋转、错切矩阵或骨骼驱动等机制。 -
变形过于突兀
当前变形缺乏过渡逻辑,一旦头部稍有移动,身体就立刻拉伸到新位置,没有平滑动画或缓动效果,看起来非常“生硬”。
成果与进展
- 渲染系统支持二维变换矩阵的向量式表达;
- 数据结构中加入了自定义
xAxis
/yAxis
,每个实体可以携带自身的变换向量; - 渲染流程中已完成
PushBitmap
接口扩展,使得图像可以在任意坐标轴方向上进行拉伸; - 拉伸效果成功联动头部位置实现了基础功能演示。
后续计划与改进方向
- 美术资源对齐:需要重新处理图像资源,确保各个身体部位的枢轴点符合逻辑动画系统的要求,尤其是头部、身体的连接部位。
- 增加动画插值:为拉伸过程增加插值或缓动算法,让变形过程更自然顺畅。
- 引入更复杂的形变模型:考虑使用矩阵变换实现旋转、错切等更自然的变形方式,甚至可以考虑简化版骨骼动画。
- 优化调试与参数调整方式:提供更方便的调试工具与可调参数接口,便于微调效果。
整体来说,我们已经完成了变形系统的雏形搭建,现在最大的问题在于资源与逻辑的不匹配以及变形效果的生硬感。随着这些问题逐步得到修复,角色的动态表现将变得更加自然和丰富,基础系统已经为进一步优化打下了坚实基础。
发现一个问题怎么躯干和衣服分离了
你会在最终决定前尝试旋转与剪切的视觉对比吗?
我们当前面临的挑战是在角色形变时,如何以最自然、最不破坏原始美术风格的方式实现视觉拉伸。简单的拉伸在视觉上不够理想,因此我们正在探索更加复杂和灵活的处理方式。
当前思路与计划
-
对比旋转与错切(Shearing)
我们计划在视觉上进行对比试验,比较使用“旋转”与“错切”两种方式对身体图形进行变换,看哪种方式在不同角度下表现更自然。- 旋转可以保持物体整体结构,但在大角度偏移时可能会导致视觉上的“不真实”;
- 错切可以制造一种更符合透视规律的假象,但容易引起图像失真。
-
最终采用混合策略
可能的最终方案是旋转 + 错切的组合:- 小角度时以旋转为主;
- 大角度时引入错切来保持角色动作的视觉连贯性;
- 两者动态权重调整,以获得最佳的视觉稳定性和自然度。
-
图像帧扩展计划
除了实时的几何变换之外,我们也考虑引入更多帧的角色图片,特别是在角色偏转幅度较大的方向:- 当前图像帧数量较少,在极端动作下无法准确表达动作状态;
- 增加“额外帧”用于视觉上的替换,尤其在身体或头部偏移较大时自动切换;
- 这种方式可以掩盖部分实时变形带来的问题。
目标与期望结果
-
最大限度减少视觉畸变
通过旋转和错切的适当组合,在保留角色结构的前提下获得视觉上的自然延展效果。 -
增强视觉表现力
借助额外的图像帧,让角色在大角度移动时呈现更具表现力和动态感的动作。 -
为后续动画系统扩展打下基础
当前这些处理可以作为未来动画过渡系统的基础,不论是引入骨骼动画,还是更复杂的图层系统,都可以从中受益。
整体来说,我们正迈入视觉表现细化阶段,从简单的线性形变逐渐向更丰富的变换策略靠拢。通过混合使用旋转与错切,并适当引入更多图像帧,我们力求在现有渲染系统架构下实现一个既真实又自然的视觉效果。
你是因为软件渲染器中已经实现了这个,所以才按这个方式写比例/剪切吗?这些变换通常不是在顶点着色器中完成的吗?
我们当前实现的这一套变换逻辑(包括位置、缩放、旋转、错切等),虽然是直接在软件渲染器中完成的,但如果将其移植到 GPU 渲染管线中,这些操作并不会放在 顶点着色器(Vertex Shader) 中处理,而是更适合在 几何着色器(Geometry Shader) 中执行。
关键理解
-
顶点着色器不适合处理复杂图元级变换
顶点着色器主要作用是处理单个顶点的变换,例如将模型空间坐标转换为裁剪空间坐标(常用于矩阵变换)。它并不适合处理“围绕中心点缩放一张位图”这类需要多个顶点协作的变换操作。 -
几何着色器才是合适的位置
我们当前做的这些逻辑(例如通过坐标轴重新计算四个角点的位置、旋转或错切一张贴图的实际绘制位置)正是几何着色器的典型用途:- 几何着色器以一个图元(如三角形、线段)为单位处理;
- 可以动态生成新顶点,例如四边形的四个顶点;
- 可以在着色器内部基于参数动态地计算出最终顶点位置;
- 非常适合渲染“变形位图”、“拉伸角色部件”、“动态几何合成”等需求。
实际架构对应关系(CPU → GPU)
软件渲染器中的组件 | GPU 渲染流程中对应位置 |
---|---|
RenderEntry_Bitmap | 发送到 GPU 的渲染批次结构体 |
位图的几何变换(位置/缩放/旋转) | 几何着色器中的顶点计算 |
width 和 height | 作为 uniform 或属性传入 Shader |
UV 坐标(最小和最大值) | 在几何着色器中生成或传入 |
例如,在几何着色器中我们会:
- 接收贴图的中点位置、宽高、旋转角度、对齐点等;
- 计算出四个顶点在世界空间中的实际位置;
- 同时输出 UV 坐标映射;
- 最终由片段着色器采样贴图颜色进行渲染。
当前做法的优势与迁移路径
- 当前在软件渲染器中完成这些处理,使得逻辑清晰、调试方便;
- 一旦需求稳定,可以非常自然地将这些逻辑迁移到 GPU;
- 几何着色器可直接复用现有代码逻辑,变化不大;
- 性能可获得巨大提升,尤其在渲染批次较多时。
总结来说,我们现在在软件渲染器中实现的所有贴图变换逻辑,未来在转向 GPU 管线时,都应直接迁移到几何着色器中,而不是顶点着色器。这样可以更高效、更符合现代图形渲染架构,也便于后续扩展更多图元级动画和视觉效果。
其他一些游戏也用过类似的移动风格,但它们是用一个悬浮的指示器,而不是把头和身体分离。你怎么看?
我们当前所设计的游戏机制,决定了必须实际移动角色的头部,而不能像其他游戏那样,仅仅使用一个**悬浮的指示器(floating indicator)**来代表头部的朝向或意图。
为何不能使用悬浮指示器的理由:
-
核心机制依赖真实的身体部件变换
游戏的动作风格与视觉反馈是建立在角色身体部分真实变形与位移上的,头部移动不仅仅是视觉提示,而是物理与动画驱动的直接体现。这种方式能提供更紧密的控制感与更有趣的视觉表现。 -
角色行为与逻辑绑定头部位置
系统中的一些逻辑可能会依赖头部的精确位置,比如视线判断、技能释放、判定区域等,这些都不能通过一个脱离实体的“标记点”来替代。 -
浮动指示器降低表现力
如果只使用悬浮提示器,会削弱当前风格所追求的动态表现和肢体语言的丰富度。角色头部的实际运动带来了情绪、动作强度等更加自然的表现,而不是简单地“提示”方向。 -
身体结构的动画联动需要真实变形
当前我们已经实现了身体部件的伸缩、错切、旋转等复杂操作,用以呈现出“头部相对于身体偏移”的视觉效果。如果仅用一个提示器,就失去了这种动画驱动结构形变的机会。
悬浮指示器方式的适用场景(我们不采用的)
虽然浮动指示器在一些策略类、MOBA、或低拟真度动作游戏中常见,但那些通常只是为了降低开发复杂度或简化动画系统。对于本项目当前的方向,这种做法显然不合适,会破坏我们追求的视觉一体性和系统连贯性。
结论:
我们坚持使用真实的头部位移,而非浮动提示器,是为了强化表现力、保持系统一致性、并支持后续复杂动画与交互逻辑。这是当前游戏风格与设计所必需的一部分。
用于碰撞检测时也会有倾斜的碰撞盒吗?还是只使用 AABB(轴对齐边界盒)?
关于碰撞检测,我们的计划是:
英雄的碰撞检测将不是物理几何层面的准确检测
我们不会对英雄的实际身体,特别是经过**错切(shear)或变形(stretch/skew)**处理后的身体部位,进行真正的几何级别的碰撞检测。这意味着:
- 不会有随错切变形而实时调整的碰撞盒(比如斜矩形或倾斜的包围盒等)
- 不会构建带角度或旋转状态的变形包围盒(Skewed Bounding Boxes)
- 甚至可能不会为角色身体使用传统轴对齐包围盒(AABB)
碰撞判断将以“语义性”为主(semantic-based)
也就是说:
- 碰撞系统不会基于角色当前渲染形状进行精确检测
- 角色的行为、动画与输入控制之间的逻辑联动,更多是通过“事件驱动”、“状态触发”来表达,而非纯粹的物理模拟
- 英雄的形变、头身分离等视觉特效不会引发额外的碰撞反应
举例:
- 英雄身体被拉伸并不会影响判定范围
- 攻击、移动等行为会以固定区域或逻辑点位为基础进行判断,而非基于变形的精确位置
好处:
- 避免了为每一帧动画重新计算变形后碰撞体的复杂逻辑
- 渲染系统和碰撞系统得以解耦,提升性能和开发效率
- 允许我们自由地变形角色视觉表现,而不必担心其带来的碰撞连锁反应
结论:
英雄本体将不会执行真实物理级别的碰撞检测,尤其不会因身体的错切变形而使用斜形或旋转包围盒。碰撞检测更倾向于逻辑触发或语义标记,而非几何形状分析。整个系统优先保障表现力和控制流畅性,而非高度物理真实性。
所以我们是以瓦片为单位移动?我还没看到新的移动方式。
我们已经完成了对房屋地砖(house tile)的位置移动调整,目前这部分已经更新完成,但还没有进行新的移动效果的完整查看。与此同时,还有一些与角色表现相关的改动正在进行中:
房屋地砖的移动已迁移
- 房屋地砖系统(house tile)已经完成了迁移或结构调整
- 具体的可视化结果尚未进行完整验证,但逻辑层面上已经完成对其位置、渲染机制的更改
后续与角色动作(Movement)相关的逻辑仍待集成
- 当前角色的移动机制还未与新的地图地砖系统完全绑定
- 尚未全面测试角色在新 house tile 上的移动是否正常、是否存在边界对齐或错位等问题
后续可能集成的内容:
- 进一步完善角色与地图之间的逻辑融合,如碰撞、行走、状态同步等
- 对新的房屋 tile 区域的美术和物理表现进行测试和微调
- 动态效果如 Bloom 或环境氛围(Garland Bloom)将进一步丰富视觉体验
总结:我们已完成对房屋地砖的基础迁移,但角色移动在新环境下的表现尚未全面查看。后续将继续推进角色行为与环境系统的整合,包含如“鸟 Amina”这样的个体角色或特效元素。整体仍处于系统扩展与验证阶段。
这些东西最终会转移到着色器中去吗?
我们目前的图形渲染依旧采用软件实现的方式,这种方式的优势在于可以最大限度地避免对特定图形 API(如 OpenGL)产生依赖,从而增强代码的可理解性与可维护性。以下是目前关于渲染策略与未来可选方向的详细说明:
当前渲染策略:优先软件实现
- 我们选择通过软件光栅化的方式手动实现所有图形变换与渲染逻辑(如旋转、缩放、剪裁等)
- 这样做的主要目的是使渲染原理变得可控且易于教学与理解
- 软件渲染的代码具有可迁移性,不依赖于某一图形 API 的生命周期或兼容性
不优先引入 OpenGL 或其它图形 API
- 图形 API(如 OpenGL、DirectX、Vulkan 等)变化频繁,生命周期短,不适合作为长期依赖基础
- OpenGL 编程本质上属于“API 调用式”开发,不涉及底层图形原理的学习
- 与其教用户怎么调用特定函数,不如直接说明图形变换与渲染过程本身,让人真正掌握原理
如果未来有性能需要,再迁移到 Shader
- 当前性能满足要求的前提下,不会将渲染逻辑迁移到 Shader 中(如顶点着色器、几何着色器等)
- 若未来确有性能瓶颈,理论上可以把这些渲染逻辑搬运到 GPU 端,通过 Geometry Shader 实现
- 现有的软件光栅代码完全可以一比一翻译为 Shader 程序,逻辑一致、变换清晰
教学目标明确:掌握原理而非 API
- 我们的重点在于让开发者掌握 图形渲染本身的机制与构造过程
- 图形 API 的学习仅需理解如何“调用已有能力”,这是次要部分,可以在具备原理知识之后轻松学习
- 通过自己手写一个光栅器(rasterizer),再去看图形 API,就会一目了然
附带内容:新调试器未有明确更新消息
- 新的调试器相关功能暂无进一步消息或实现计划更新
总结:我们当前坚持采用软件方式实现渲染,不主动迁移到 Shader 或图形 API 的调用层面,除非性能需求迫使我们必须这么做。目的是保证代码稳定性、可维护性,以及教学价值,真正帮助开发者理解底层图形渲染原理,而不是被动依赖快速过时的图形库接口。
我怀疑着色器这个概念很快就会被淘汰
我们对当前图形编程领域中的着色器(Shaders)体系持有明确的批判态度,并对其长期可持续性表示怀疑。以下是对此的详细归纳总结:
着色器的设计模式本质上不合理
- 当前图形渲染架构中所谓“着色器”的概念本质上是人为构建的分离模型,并非出于计算本身的逻辑自然产生,而是源于不同厂商、不同时代硬件能力差异的妥协
- 由于这些差异导致着色器系统形成了大量历史包袱和逻辑混乱,发展路径极不稳定
着色器演进轨迹动荡不定
- 起初只是简单的图形 API 调用,用于配置混合模式等状态设置
- 后来出现类似汇编语言的程序,提供极为有限的操作集,受限于硬件流水线结构
- 再后来演进为 C 风格语言(如 GLSL、HLSL 等),可编写逻辑更复杂的程序段
- 最近几年又出现了一些通用编程模型(如 compute shaders 或 WebGPU 中的 WGSL)试图打破阶段划分
- 总体趋势是不断推翻旧模型,重新设计新架构,几乎每两三年就有一次彻底的范式迁移
着色器结构割裂且缺乏统一编程模型
- 当前的着色器阶段(顶点着色器、几何着色器、片段着色器等)人为划分、不自然
- 每一阶段的输入输出规则复杂,功能限制严重,并非一种具备良好抽象性的通用模型
- 若该模型合理,那么 CPU 编程也应该是类似的“阶段式”程序,但实际上我们写的是统一连续逻辑结构的代码
现行模式不可持续,最终必须被取代
- 真正可持续的图形编程模型应当类似于 CPU 架构:定义一套逻辑清晰的计算模型,然后通过 ISA(指令集架构)暴露给开发者
- 统一模型可以通过更标准化的方式来编程,不再被零散的状态管理和 API 混乱束缚
- 未来的 GPU 编程方向应趋向“通用计算模型”,而不是不断翻新“临时兼容性解决方案”
未来几年着色器形式将剧变
- 我们不认为现行着色器架构会在 5 年后仍以当前形态存在
- 极有可能被某种更合理、更统一的系统所取代,甚至连“阶段”概念都会被整合或消解
- 未来架构或许不再将图形管线细分为特定阶段,而是采用更灵活的任务调度和数据流控制模型
编程教育应聚焦原理而非 API
- 鉴于上述混乱,我们倾向于避免教授图形 API 层面的细节调用
- 更重要的是理解图形渲染的原理与流程本身,这样才能适应不断变化的图形体系
- 理解了底层的实际操作与逻辑(例如如何构建变换矩阵、如何采样纹理等),将来无论 API 如何更换都能快速适配
总结
我们认为当前的着色器体系是过渡性、非可持续的解决方案,其存在源于历史妥协与技术演化过程中的权宜安排。未来的 GPU 编程模型必须朝着统一编程结构、清晰指令集架构的方向发展。现在深入研究具体的着色器语言与 API 调用细节,教学意义有限,更应将重点放在理解渲染原理与计算逻辑上,从而为未来架构的变迁做好准备。
你觉得你会把前几天写的弹簧代码用在相机移动上吗?我觉得临界阻尼的弹簧机制会很适合那种需求。
我们在讨论摄像机移动时,提到了使用弹簧系统来实现平滑过渡的问题。以下是这一部分内容的详细中文总结:
摄像机移动应采用临界阻尼弹簧系统
- 临界阻尼弹簧(Critically Damped Spring) 是在摄像机移动中最理想的方案,可以实现快速而不晃动的平滑过渡
- 我们强烈反对使用欠阻尼弹簧(Underdamped Spring),因为那种方式在摄像机追踪过程中会产生来回震荡的“回摆”现象,极度干扰体验
欠阻尼弹簧的负面影响
- 使用欠阻尼弹簧的摄像机会在目标点周围不断震荡,给人一种“头晃动”的错觉
- 这种视觉反馈非常容易造成晕动症(motion sickness)
- 曾有游戏(如 Stephen’s Sausage Roll)采用该方式,导致游玩过程中产生恶心、眩晕感,体验非常糟糕
- 市面上存在大量游戏也犯了类似错误,导致玩家视觉体验受损
替代方案:B样条(B-Spline)或临界阻尼模型
- 虽然未完全确定,但我们倾向于使用更稳定的曲线插值方法,例如B样条(B-Spline)
- 也有可能最终还是选择临界阻尼弹簧模型,因为其在动态响应和平滑性方面表现也非常好
- 不管选择哪种方式,关键点是不能有任何震荡行为,必须稳定、平顺
总结
摄像机的跟踪逻辑必须高度平稳,任何来回弹动的行为都将严重破坏游戏体验,甚至引发身体不适。我们明确禁止使用欠阻尼弹簧系统进行摄像机控制,倾向使用临界阻尼弹簧或曲线插值(如 B-spline)等更加平滑的运动控制策略,以确保视觉体验自然、舒适、不晃动。
我之前做相机时通常是用 x += (x - dest) / 10
这样,算是啥?
我们在讨论摄像机平滑移动的算法设计时,提到了一种常见但有问题的写法:
X += (X - desiredX) / 10;
接着对这种写法进行了详细质疑和分析,总结如下:
这种写法缺乏时间因子
- 上述写法在形式上看似是让当前位置逐渐趋近于目标位置(desiredX)
- 但是这段代码完全没有考虑时间变量 T(delta time)
- 没有时间因子意味着这个系统不具备真实物理意义上的稳定性与可预测性
- 缺乏时间尺度后,系统的动态响应会完全取决于帧率(FPS),在不同硬件上运行结果会不同
正确的物理系统中必须包含时间因子
-
类似摄像机跟踪、物体插值、移动平滑等系统,本质上是一个连续动态系统
-
应该使用物理模型,例如弹簧系统,形式如下:
dx/dt = 速度 d²x/dt² = -k(x - 目标位置) - c*速度
-
这类系统需要通过时间积分来获得离散帧之间的状态变更
-
时间步长 T 是核心组成部分,决定了模拟的连续性和稳定性
总结
简单写法 X += (X - desiredX) / 10
看似有效,但实际上忽略了关键的时间因子 T,是一个伪物理系统。这种做法仅在特定帧率下勉强可用,但在帧率变化或多平台移植的情况下,表现会极不稳定,容易出现忽快忽慢、跟踪不准等问题。
如果希望摄像机或其他插值系统表现出平滑、稳定、可控的行为,必须引入明确的时间因子 T,并基于真实物理模型(如临界阻尼弹簧)来构建运动方程。这才是可靠、专业、可扩展的方式。
那没有考虑时间 t,是瞬时变换
我们在探讨一种常见的摄像机移动或插值算法时,进一步分析了其存在的问题。总结如下:
该方法的核心逻辑
-
使用了如下形式的更新方式:
X += (目标位置 - 当前X) / 10;
-
其含义是在每一帧中将当前位置向目标位置移动 1/10 的距离。
主要问题:与时间无关的插值
- 该算法忽略了时间步长(delta time),即无论帧率如何,都每帧移动 1/10 距离。
- 这种写法不具备物理一致性:在 30 FPS 和 144 FPS 下行为截然不同,容易造成体验不一致。
永远到不了目标 —— 近似“芝诺悖论”
-
由于每帧移动的距离是当前位置和目标位置的 1/10,因此:
- 总是距离目标越来越近,但永远不完全到达目标(理论上)。
- 实际上因为浮点数精度限制,最终会“收敛”至目标,但这是一种数值收敛,不是逻辑收敛。
-
类似“芝诺悖论”:一步比一步近,但总有剩余距离,始终没有“跨出最后一步”。
正确的方式:基于时间的插值或物理模拟
-
应该引入时间步长 delta_time,使系统响应具有真实的时间行为。
-
例如使用指数衰减插值(Exponential Smoothing):
float smoothing = 1.0f - pow(0.01f, delta_time); X += (目标位置 - X) * smoothing;
- 这种方式会随时间以恒定速率趋近目标,不依赖帧率。
- 还能确保在指定时间范围内快速收敛到目标。
-
或者更准确地使用临界阻尼弹簧系统来控制移动,更符合物理直觉和用户体验。
总结
我们探讨了一种“每帧靠近目标一定比例”的插值方法,并指出其关键问题在于缺乏时间因子,属于帧率依赖型算法,容易导致摄像机行为不一致或迟迟无法到达目标的“数值陷阱”。我们建议改用基于时间的插值或物理模拟系统,如指数衰减或临界阻尼弹簧,以确保行为稳定、流畅、符合直觉。
我猜应该是 (dest - x)
吧?
当讨论到移动算法时,指出了一个问题:如果使用了 X - 目标位置
的形式更新位置,那就会导致物体远离目标,而不是接近目标。
关键问题:错误的更新方式
X - 目标位置
的方式将导致物体每次朝着远离目标的方向移动。- 这种方式等同于**“固定步长”**,实际上是将物体从目标位置推开,而不是朝目标移动。
正确的方式:靠近目标
- 应该使用
目标位置 - 当前X
的方式,让物体逐步靠近目标位置。 - 这样才会让物体每次都朝着目标前进,而不是远离目标。
总结
如果使用 X - 目标位置
的方式更新位置,就会导致物体每次都离目标更远,无法实现朝目标移动的目标。正确的做法应该是 目标位置 - 当前X
,这样才能确保物体始终朝目标靠近。
我以前是做固定帧率的,所以还能用,现在已经改了。我查一下
在讨论FPS(每秒帧数)时,提到了调整相机或物体移动时,使用了某种对数或指数函数来平滑运动。这种方法通过逐渐减少每帧的移动量,避免了过快或过慢的跳跃式运动,使得移动更加自然流畅。
主要内容总结:
- FPS的调整方法:通过对数或指数函数对相机或物体的移动进行调整,确保运动既不突然又不过于缓慢。
- 函数作用:这种方式能帮助在移动过程中实现更加平滑的过渡,避免了线性移动的生硬感。
结论
这种对数或指数函数的使用,可以大大提升FPS体验,提供更加平滑且连贯的运动感,避免了因过快或过慢的变化带来的不适。
黑板讲解:GarlandoBloom 的相机移动方法
在讨论相机或物体移动的过程中,提到了通过距离和时间的关系来控制运动的方式。通过将距离除以某个常数来设定每帧的移动量,初始阶段的移动量最大,而随着时间的推移,移动量会逐渐减少。这种方法会导致一个渐近的运动曲线,类似于一个双曲线,即最初的移动非常快速,但随着接近目标,速度会不断减慢,最终几乎无法达到目标,除非由于浮点数精度的原因会四舍五入。
然而,这种方法并不理想,因为它没有考虑到加速过程。刚开始时,相机会瞬间加速,快速向目标移动,然后进入减速阶段,使得运动变得不自然。理想的方式是相机在运动初期应当有一定的加速度,然后逐渐减速,达到平滑过渡。
这种方法的曲线应该是一个S形的加速后减速的曲线,而不是一开始就有巨大的加速。否则,整个运动过程会显得非常突兀,并且会让用户感到不舒服。
总结来说,尽管通过距离除以常数来进行运动控制可以产生一个快速的初始移动,但没有加速度的方式会导致不自然的体验,因此更推荐采用有加速度的平滑曲线来实现更自然的运动效果。
你打算在英雄被打或巨石落下时添加相机震动、模糊等效果吗?
在讨论相机震动或模糊效果时,提到如果英雄被击中或巨石掉落到英雄附近,可能会加入相机震动或模糊效果。这类效果通常属于精细化的打磨内容,并不是最初需要重点考虑的部分。尽管如此,这些效果的实现相对简单,并且对游戏的沉浸感提升有帮助。
具体来说,相机震动可以通过简单的技术实现,不需要太多复杂的操作。因此,虽然目前不一定需要立刻加入,但它们可以在后期作为额外的细节来增强游戏的表现。
相关文章:
游戏引擎学习第275天:将旋转和剪切传递给渲染器
回顾并为今天的内容定下基调 我们认为在实现通用动画系统之前,先学习如何手写动画逻辑是非常有价值的。虽然加载和播放预设动画是合理的做法,尤其是在团队中有美术人员使用工具制作动画的情况下更是如此,但手动编写动画代码能让我们更深入理…...
conda 输出指定python环境的库 输出为 yaml文件
conda 输出指定python环境的库 输出为 yaml文件。 有时为了项目部署,需要匹配之前的python环境,需要输出对应的python依赖库。 假设你的目标环境名为 myenv,运行以下命令: conda env export -n myenv > myenv_environment.ym…...
ES6 语法
扩展运算符 … 口诀:三个点,打散数组,逐个放进去 例子: let arr [1, 2];let more [3, 4];arr.push(...more); // arr 变成 [1, 2, 3, 4]解构赋值 口诀:左边是变量,右边是值,一一对应 例子&…...
BFS算法篇——打开智慧之门,BFS算法在拓扑排序中的诗意探索(下)
文章目录 引言一、课程表1.1 题目链接:https://leetcode.cn/problems/course-schedule/description/1.2 题目分析:1.3 思路讲解:1.4 代码实现: 二、课程表||2.1 题目链接:https://leetcode.cn/problems/course-schedul…...
While语句数数字
import java.util.Scanner;public class Hello {public static void main(String[] args) {Scanner in new Scanner(System.in);int number in.nextInt();int count 0;while( number > 0 ){number number / 10;count count 1;}System.out.println(count);} }...
G1JVM内存分配机制详解
为什么堆内存不是预期的3G? 当您设置-XX:MaxRAMPercentage75时,JVM并不会简单地将容器内存(4G)的75%全部分配给堆,原因如下: 计算基准差异: 百分比是应用于"可用物理内存"而非"容器总内存" &q…...
“端 - 边 - 云”三级智能协同平台的理论建构与技术实现
摘要 随着低空经济与智能制造的深度融合,传统集中式云计算架构在实时性、隐私保护和资源效率上的瓶颈日益凸显。本文提出“端 - 边 - 云”三级智能协同平台架构,以“时空 - 资源 - 服务”三维协同理论为核心,构建覆盖终端感知、边缘计算、云端…...
【UAP】《Empirical Upper Bound in Object Detection and More》
Borji A, Iranmanesh S M. Empirical upper bound in object detection and more[J]. arXiv preprint arXiv:1911.12451, 2019. arXiv-2019 文章目录 1、Background and Motivation2、Related Work3、Advantages / Contributions4、Experimental Setup4.1、Benchmarks Dataset…...
Web Service及其实现技术(SOAP、REST、XML-RPC)介绍
一.概述 1.Web Service(Web 服务) Web Service 由万维网联盟 (W3C) 定义为一种软件系统,旨在支持通过网络进行可互操作的计算机间交互。 广义概念:基于 Web 技术(如 HTTP 协议)的跨平台、跨语言通信机制…...
基于Spring Boot+Layui构建企业级电子招投标系统实战指南
一、引言:重塑招投标管理新范式 在数字经济浪潮下,传统招投标模式面临效率低、透明度不足、流程冗长等痛点。本文将以Spring Boot技术生态为核心,融合Mybatis持久层框架、Redis高性能缓存及Layui前端解决方案,构建一个覆盖招标代理…...
【嵌入式】记一次解决VScode+PlatformIO安装卡死的经历
PlatformIO 是开源的物联网开发生态系统。提供跨平台的代码构建器、集成开发环境(IDE),兼容 Arduino,ESP8266和mbed等。 开源库地址:https://github.com/platformio 在 VScode 中配置 PlatformIO 插件,记录…...
抗量子计算攻击的数据安全体系构建:从理论突破到工程实践
在“端 - 边 - 云”三级智能协同理论中,端 - 边、边 - 云之间要进行数据传输,网络的安全尤为重要,为了实现系统总体的安全可控,将构建安全网络。 可先了解我的前文:“端 - 边 - 云”三级智能协同平台的理论建构与技术实…...
【FMMT】基于模糊多模态变压器模型的个性化情感分析
遇到很难的文献看不懂,不应该感到气馁,应该激动,因为外审估计也看不太懂,那么学明白了可以吓唬他 缺陷一:输入依赖性与上下文建模不足 缺陷描述: 传统自注意力机制缺乏因果关系,难以捕捉序列历史背景多模态数据间的复杂依赖关系未被充分建模CNN/RNN类模型在…...
力扣Hot100(Java版本)
1. 哈希 1.1 两数之和 题目描述: 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案,并且你不能使用两次相同…...
Stream流简介、常用方法
Stream流的三类方法 获取Stream流 创建一条流水线,并把数据放到流水线上准备进行操作 中间方法 流水线上的操作一次操作完毕之后,还可以继续进行其他操作 终结方法 一个Stream流只能有一个终结方法是流水线上的最后一个操作 生成Stream流的方式 Collec…...
C# 集成 FastDFS 完整指南
1. 环境准备 (1) 安装 FastDFS 服务端 部署 Tracker 和 Storage 节点,确保服务正常运行。 配置 tracker_server 地址(如 192.168.1.100:22122)。 (2) 添加 NuGet 包 通过 NuGet 安装 FastDFS 客户端库: Install-Pack…...
重构门店网络:从“打补丁“到“造地基“的跨越
您是否遇到过这样的窘境? 新店开张要等一周,就为装根网线; 偏远地区门店三天两头断网,顾客排长队却结不了账; 总部想看实时数据,结果收到一堆乱码报错; 总部ERP系统升级,2000家门…...
TI的ADS1291代替芯片LH001-99
血管疾病严重威胁人类生命健康安全,随着人口老龄化进程的加快和社会压力等因素的增加,患病率正呈现逐年上升趋势,并且越来越年轻化。然而,心血管疾病大多由器官器质性病变引起,一旦患病很难完全康复,需要进…...
NPOI 操作 Word 文档
管理 NuGet 程序包 # word操作 NPOI# 图片操作 SkiaSharp Controller代码 using Microsoft.AspNetCore.Mvc; using NPOI.Util; using NPOI.XWPF.Model; using NPOI.XWPF.UserModel; using SkiaSharp;namespace WebApplication2.Controllers {[Route("api/Npoi/[action]…...
css3基于伸缩盒模型生成一个小案例
css3基于伸缩模型生成一个小案例 在前面学习了尚硅谷天禹老师的css3内容后,基于伸缩盒模型做的一个小案例,里面使用了 flex 布局,以及主轴切换,以及主轴平分等特性,分为使用css3 伸缩盒模型方式,已经传统的…...
精简大语言模型:用于定制语言模型的自适应知识蒸馏
Streamlining LLMs: Adaptive Knowledge Distillation for Tailored Language Models 发表:NAACL 2025 机构:德国人工智能研究中心 Abstract 诸如 GPT-4 和 LLaMA-3 等大型语言模型(LLMs)在多个行业展现出变革性的潜力…...
Rollup入门与进阶:为现代Web应用构建超小的打包文件
我们常常面临Webpack复杂配置或是Babel转译后的冗余代码,结果导致最终的包体积居高不下加载速度也变得异常缓慢,而在众多打包工具中Rollup作为一个轻量且高效的选择,正悄然改变着这一切,本文将带你深入了解这个令人惊艳的打包工具…...
博客系统技术需求文档(基于 Flask)
以下内容是AI基于要求生成的技术文档,仅供参考~ 🧱 一、系统架构设计概览 层级 内容 前端层 HTML Jinja2 模板引擎,集成 Markdown 编辑器、代码高亮 后端层 Flask 框架,RESTful 风格,Jinja2 渲染 数据库 SQLi…...
快速排序、归并排序、计数排序
文章目录 前言一、归并排序算法逻辑递归实现非递归实现 二、快速排序算法介绍递归实现非递归实现算法的一种优化—三路划分法 四、计数排序算法原理代码实现优劣分析 五、排序算法的性能比较总结 前言 本文介绍这三种非常强大的排序算法,每种算法都有各自的特点、不…...
python语言与地理处理note 2025/05/11
1. 函数定义必须要在调用之前 (1)正确示例: def test():print("what a wonderful world!")test() (2)错误示例: test() def test():print("what a wonderful world!") 会报错&…...
贪心算法:最小生成树
假设无向图为: A-B:1 A-C:3 B-C:1 B-D:4 C-D:1 C-E:5 D-E:6 一、使用Prim算法: public class Prim {//声明了两个静态常量,用于辅助 Prim 算法的实现private static final int V 5;//点数private static final int INF Integer.MA…...
免费 OCR 识别 + 批量处理!PDF 工具 提升办公效率
各位办公小能手们!今天给你们介绍一款超厉害的软件——PDF工具V2.2!我跟你们说,这玩意儿就像是PDF界的超级英雄,专门搞定PDF文件的编辑、转换、压缩这些事儿。 先说说它的核心功能哈。基础文档管理方面,它能把好几个PD…...
尼康VR镜头防抖模式NORMAL和ACTIVE的区别(私人笔记)
1. NORMAL 模式(常规模式) 适用场景:一般手持拍摄,比如人像、静物、风景或缓慢平移镜头(如水平追拍)等。工作特性: 补偿手抖引起的小幅度震动(比如手持时自然的不稳)&am…...
在scala中sparkSQL读入csv文件
以下是 Scala 中使用 Spark SQL 读取 CSV 文件的核心步骤和代码示例(纯文本): 1. 创建 SparkSession scala import org.apache.spark.sql.SparkSession val spark SparkSession.builder() .appName("Spark SQL Read CSV") …...
swift flask python ipad当电脑键盘 实现osu x键和z键 长按逻辑有问题 quart 11毫秒
键盘不行我5星都打不过,磁轴不在身边 127.0.0.1不行要用192.168哪个地址 from flask import Flask from pynput.keyboard import Controller from threading import Threadapp Flask(__name__) keyboard Controller()# 按下按键 app.route("/press_down/<…...
浅论3DGS溅射模型在VR眼镜上的应用
摆烂仙君小课堂开课了,本期将介绍如何手搓VR眼镜,并将随手拍的电影变成3D视频。 一、3DGS模型介绍 3D 高斯模型是基于高斯函数构建的用于描述三维空间中数据分布概率的模型,高斯函数在数学和物理领域有着广泛应用,其在 3D 情境下…...
React状态管理-对state进行保留和重置
相同位置的相同组件会使得 state 被保留下来 当你勾选或清空复选框的时候,计数器 state 并没有被重置。不管 isFancy 是 true 还是 false,根组件 App 返回的 div 的第一个子组件都是 <Counter />: 你可能以为当你勾选复选框的时候 st…...
嵌入式STM32学习——外部中断EXTI与NVIC的基础练习⭐
按键控制LED灯 按键控制LED的开发流程: 第一步:使能功能复用时钟 第二布,配置复用寄存器 第三步,配置中断屏蔽寄存器 固件库按键控制LED灯 外部中断EXTI结构体:typedef struct{uint32_t EXTI_Line; …...
git merge和git rebase
git merge和git rebase 在Git中merge和rebase都是git在管理整合分支的两种主要工具,但是他们的工作方式、提交历史影响和使用场景不同。 git merge 定义 将两个分支的提交历史合并,创建一个新的合并提交(merge commit)ÿ…...
我的MCP相关配置记录
1.VSCode的Cline中的MCP {"mcpServers": {"github.com/modelcontextprotocol/servers/tree/main/src/github": {"autoApprove": [],"disabled": false,"timeout": 60,"command": "cmd","args&quo…...
浅聊一下数据库的索引优化
背景 这里的索引说的是关系数据库(MSSQL)中的索引。 本篇不是纯技术性的内容,只是聊一次性能调优的经历,包含到一些粗浅的实现和验证手段,所以,大神忽略即可。 额…对了,笔者对数据库的优化手段…...
如何创建maven项目
1.IDEA 中创建 Maven 项目 步骤一:点击 File -> New -> Project,在弹出的窗口左侧选择 Maven,点击 Next: 步骤二:填写项目的 GroupId、ArtifactId、Version 等信息(这些对应 pom.xml 中的关键配置&am…...
LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS
一、引言 在自然语言处理领域,大规模预训练语言模型(LLMs)展现出强大的语言理解和生成能力。然而,将这些模型适配到多个下游任务时,传统微调方法面临诸多挑战。LoRA(Low-Rank Adaptation of Large Language Models)作为一种创新的微调技术,旨在解决这些问题,为大语言…...
Conda在powershell终端中无法使用conda activate命令
主要有以下原因: Windows PowerShell安全策略:默认情况下,PowerShell的执行策略设置为"Restricted",这会阻止运行脚本,包括conda的初始化脚本。调用方式不同:在PowerShell中,需要使用…...
MySQL索引底层数据结构与算法
1、索引的数据结构 1.1、二叉树 1.2、红黑树(二叉平衡树) 1.3、hash表 对key进行一次hash计算就可以定位出数据存储的位置 问题:hash冲突问题、仅满足和in的查找,不支持范围查找 1.4、B-tree 1.5、B tree 非叶子节点不存储data&…...
GOOSE 控制块参数gocbRef及goID有大小写要求
在 IEC 61850 标准中,GOOSE 控制块参数gocbRef和goID的大小写是严格区分的。这一结论基于以下多维度分析: 一、标准协议与配置文件的强制性 XML 语法的刚性约束 GOOSE 控制块的配置信息通过 SCL(Substation Configuration Languageÿ…...
重庆医科大学附属第二医院外科楼外挡墙自动化监测
1.项目概述 重庆医科大学附属第二医院,重医附二院,是集医疗、教学、科研、预防保健为一体的国家三级甲等综合医院。前身为始建于1892年的“重庆宽仁医院”。医院现有开放床位 1380张,年门诊量超过百万人次,年收治住院病人4.5万人…...
3.4 数字特征
本章系统讲解随机变量的数字特征理论,涵盖期望、方差、协方差与相关系数的核心计算与性质。以下从四个核心考点系统梳理知识体系: 考点一:期望(数学期望) 1. 离散型随机变量的数学期望 一维情形: E ( X …...
servlet-api
本次内容总结 1、再次学习Servlet的初始化方法 2、学习Servlet中的ServletContext和<context-param> 3、什么是业务层 4、IOC 5、过滤器 7、TransActionManager、ThreadLocal、OpenSessionInViewFilter 1、再次学习Servlet的初始化方法 1)Servlet生命周期&…...
NLTK进行文本分类和词性标注
《python ⾃然语⾔处理实战》学习笔记 NLTK 下载依赖 !pip install nltkimport nltk nltk.download(punkt_tab)分词(tokenize) from nltk.tokenize import word_tokenize from nltk.text import Textinput_str """Twinkle, twinkle, little star, How I won…...
电机控制储备知识学习(一) 电机驱动的本质分析以及与磁相关的使用场景
目录 电机控制储备知识学习(一)一、电机驱动的本质分析以及与磁相关的使用场景1)电机为什么能够旋转2)电磁原理的学习重要性 二、电磁学理论知识1)磁场基础知识2)反电动势的公式推导 附学习参考网址欢迎大家…...
华三路由器单臂路由配置
目录 1.实验目的1.1 掌握华三路由器单臂路由配置方法2.1 路由器连接交换机,交换机划分多个 VLAN,不同 VLAN 的 PC 通过路由器实现通信 配置步骤与命令解析1.配置交换机2.配置路由器验证配置3.1 配置交换机 VLAN3.1.1 创建 VLAN3.1.2 配置端口所属 VLAN3.…...
一键转换上百文件 Word 批量转 PDF 软件批量工具
各位办公族们,你们有没有被手动把Word一个个转成PDF给折腾得欲哭无泪过啊?我之前就因为这事忙得晕头转向,眼睛都快看瞎了!不过呢,后来我发现了专门为咱提升办公效率设计的Word批量转PDF软件,那简直就是办公…...
矫平机:工业精密矫正的全维度解析
作为现代制造业的核心设备之一,矫平机通过消除材料残余应力、提升平整度,持续推动着汽车、航空航天、新能源等领域的质量升级。本文基于最新行业动态与技术突破,从原理革新到智能化实践展开深度解析。 一、核心原理:力学与智能的深…...
网络安全-等级保护(等保) 2-3 GB/T 22240—2020《信息安全技术 网络安全等级保护定级指南》-2020-04-28发布【现行】
################################################################################ 在开始等级保护安全建设前,第一步需要知道要保护的是什么,要保护到什么程度,所以在开始等级保护中介绍的第一个标准是《定级指南》,其中明确了…...