游戏开发图形学【免费下载链接】turbulenz_engineTurbulenz is a modular 3D and 2D game framework for making HTML5 powered games for browsers, desktops and mobile devices.项目地址https://gitcode.com/gh_mirrors/tu/turbulenz_engine点击查看免费下载Turbulenz Engine物理设备 API 文档在碰撞检测时会产生接触点Contact数据本文以官方 Contact 对象文档 为骨架结合 physicsdevice.ts 的底层实现完整讲解 Contact 的四个只读属性localPointOnA、localPointOnB、worldNormalOnB、added的语义、对象池复用与禁止持有引用的生命周期约束并给出把接触点转换到世界坐标、可视化接触法线的可运行实战方案适用于碰撞反馈、受击判定、单向平台等游戏玩法开发。一、Contact 是什么物理回调的接触数据载体在 Turbulenz 的三维物理系统中当两个物体RigidBody、CollisionObject 或 Character发生碰撞时物理引擎会在仿真过程中生成若干个接触点描述两物体表面实际接触的位置与法线。一个Contact 对象就是单个接触点的信息封装它通过 physicsdevice_api.rst 中定义的**接触回调contact callback**传递给游戏逻辑。它的核心定位可以概括为A Contact object contains the contact information for a contact callback.Contact 对象包含一次接触回调所需的接触信息。也就是说Contact 不是让开发者主动查询的对象而是回调函数的参数载体——引擎检测到接触后把接触点数据打包成 Contact 对象通过回调交给你的代码处理。1.1 最需要记住的一条规则对象会被复用禁止持有引用官方文档开篇给出了一条使用 Contact 时必须遵守的硬性约束Contact objects will be reused from callback to callback. Do not keep any references to a Contact object, always copy any information you need to your own objects.Contact 对象会在每次回调之间被复用。不要持有 Contact 对象的引用任何你需要的信息都必须复制到你自己的对象中。这并非一句口头警告而是由底层实现直接决定的。在 physicsdevice.ts 中所有接触数据都通过**对象池object pool**分配与回收// tslib/webgl/physicsdevice.tsWebGLPhysicsContact 对象池 var WebGLPhysicsContact { contactPool: WebGLPhysicsContact[][], contactPoolSize: 0, contactPoolAllocationSize: 128, // 每次批量分配 128 个 allocate: function webglPhyssicsContactAllocateFn(): WebGLPhysicsContact { // 当池为空时一次性分配 52 * 128 个 float 的连续缓冲 // 切分成 128 个长度为 52 的 subarray 放进池中 ... this.contactPoolSize - 1; var contact : WebGLPhysicsContact contactPool[this.contactPoolSize]; contact[51] 1.0; // 标记为“新接触” return contact; }, deallocate: function webglPhyssicsContactDeallocateFn(contact: WebGLPhysicsContact) { this.contactPool[this.contactPoolSize] contact; this.contactPoolSize 1; // Contact jAccN is cached between updates. Needs to be reset if contact is re-used. ... } };源码注释也直接点明了设计动机// Contact objects are object pooled due to being frequently created and destroyed.接触对象由于创建和销毁极为频繁因此采用对象池管理。一个接触对象在本次回调中是contact下一次仿真步可能就是另一次碰撞的接触数据了。因此在回调中必须立即把需要的数据拷贝到自己的数组或对象中绝不能把contact存入全局变量或长期引用的数据结构里。二、四个只读属性详解Contact 对象公开四个**只读Read Only**属性。源码中WebGLPhysicsPublicContactphysicsdevice.ts通过Object.defineProperty只定义了 getteradded甚至没有 setter因此在 JavaScript/TypeScript 层面对游戏代码来说它们完全不可写。2.1localPointOnA接触点在物体 A 本地空间的位置SummaryAVector3representing the location of the contact in the local space of object A.一个表示接触点在物体 A 本地空间中位置的 Vector3。var contactOnA contact.localPointOnA;语义接触点相对于物体 A 自身坐标系即物体 A 未被平移、旋转之前的世界原点坐标系的坐标。注意它是本地空间坐标并非世界坐标。若需要世界坐标必须结合物体 A 的变换矩阵做转换见后文实战示例。取值类型Float32Array(3)形式的 Vector3由 vmath.ts 的v3Build构造。底层映射源码 getter 从内部 52 维接触数据数组的pr[0]、pr[1]、pr[2]三个分量读出Object.defineProperty(p, localPointOnA, { get : function getLocalPointOnA() { var pr this._private; return VMath.v3Build(pr[0], pr[1], pr[2]); }, ... });2.2localPointOnB接触点在物体 B 本地空间的位置SummaryAVector3representing the location of the contact in the local space of object B.一个表示接触点在物体 B 本地空间中位置的 Vector3。var contactOnB contact.localPointOnB;语义与localPointOnA对称表示接触点相对于物体 B 自身坐标系的位置对应内部数组的pr[3]、pr[4]、pr[5]三个分量。用途在实战中开发者通常以被击中的物体 B为视角处理碰撞点例如在物体 B 的本地坐标上生成命中特效、碎屑粒子或弹孔贴花此时直接使用localPointOnB再结合 B 的变换矩阵最方便。2.3worldNormalOnB物体 B 表面的世界空间法线SummaryAVector3representing the normal of the surface in world space at the contact location on object B.一个表示物体 B 上接触位置表面法线、且处于世界空间的 Vector3。var contactNormalOnB contact.worldNormalOnB;语义接触点处物体 B 表面的法线方向且已经处于世界空间可直接用于判定碰撞方向、反弹方向或绘制法线。与前面两个属性的关键差异localPointOnA/B是本地空间而worldNormalOnB是世界空间无需再做变换。底层映射对应内部数组的pr[12]、pr[13]、pr[14]三个分量数组布局注释中[12,15) : normal (vector3) (on object B)。2.4added本次仿真步中接触是否为新添加SummaryA boolean value,trueif the contact was added on the last simulation update,falseotherwise.布尔值若接触是在上一次仿真更新中被新添加的则为true否则为false。if (contact.added) { ... }语义区分新产生的接触与持续存在的接触。例如一个球在斜坡上滚动时同一批接触点会在多个仿真步中持续存在只有刚发生碰撞的那一帧added才为true。底层实现getter 直接读取内部数组的第 51 个分量并做判断——(0.0 pr[51])。在WebGLPhysicsContact.allocate分配新接触时会把contact[51]置为1.0注释// new contact而在每次回调派发结束后updateContactCallbacks会统一把这些标记清零contacts[j][51] 0; // Flag contacts as old于是下一次仿真步中旧接触的added自然变为false。用途只在接触刚发生时触发一次的逻辑如首次受击音效、得分判定应判断added避免在持续接触的每一帧都重复触发。三、深入底层52 维接触数据结构与引擎内部字段Turbulenz 的每个物理接触点并非一个普通的 JS 对象而是一个52 个浮点数组成的Float32Array。源码在 physicsdevice.ts 给出了完整的内存布局注释// [0, 3) : localA (vector3) —— 即 localPointOnA // [3, 6) : localB (vector3) —— 即 localPointOnB // [6, 9) : relA (vector3) // [9, 12) : relB (vector3) // [12,15) : normal (vector3) (on object B) —— 即 worldNormalOnB // [15,18) : tangent (vector3) // [18,21) : bitangent (vector3) // [21,22) : distance —— 接触分离距离 // [22,25) : Jacobian : normal x relA * iInertiaA (vector3) // [25,28) : Jacobian : normal x relB * iInertiaB (vector3) // [28,31) : Jacobian : tangent x relA * iInertiaA (vector3) // [31,34) : Jacobian : tangent x relB * iInertiaB (vector3) // [34,37) : Jacobian : bitangent x relA * iInertiaA (vector3) // [37,40) : Jacobian : bitangent x relB * iInertiaB (vector3) // [40,43) : jAcc (normal, tangent, bitangent) (vector3) // [43,44) : bias // [44,45) : jAccBias // [45,46) : penetration constraint, effective mass (kN) // [46,50) : friction constraint, effective mass // [50,51) : bounce // [51,52) : new —— 即 added 属性对应的标记位可以看到WebGLPhysicsPublicContact对外暴露的 Contact 对象只是这 52 个浮点数之上的视图层它的每个公开属性 getter 都从内部数据数组的特定区间取值其中localPointOnA←pr[0..2]localPointOnB←pr[3..5]worldNormalOnB←pr[12..14]added←pr[51] 0这种紧凑数组 惰性 getter的设计让物理引擎可以在热循环中以极低的 GC 压力处理接触数据代价就是接触对象本身不具备持久性——这正是第一节那条禁止持有引用规则的根源。四、Contact 从哪里来接触回调的四种触发时机Contact 对象只会在接触回调中被派发。根据 physicsdevice_api.rstRigidBody、CollisionObject 和 Character 都可以在创建时通过参数对象注册以下回调回调参数触发时机回调签名onPreSolveContact两个物体之间首次检测到新接触时发生在仿真步内function newContact(objectA, objectB, contact)单个 ContactonAddedContacts仿真步内至少新增一个接触时在仿真更新结束时触发function contactsAdded(objectA, objectB, contacts)Contact 数组onProcessedContacts仿真步内有任何接触被处理时在仿真更新结束时触发function contactsProcessed(objectA, objectB, contacts)Contact 数组onRemovedContacts仿真步内至少移除一个接触时触发两物体不再接触时以空数组调用function contactsRemoved(objectA, objectB, contacts)Contact 数组注册示例来自 physicsdevice_api.rstfunction newContact(objectA, objectB, contact) { ... } var boxObject physicsDevice.createCollisionObject({ ... onPreSolveContact: newContact ... });还可以用contactCallbacksMask过滤触发回调的碰撞组默认使用创建物体时的 mask例如只对投射物组触发var boxObject physicsDevice.createCollisionObject({ ... contactCallbacksMask: physicsDevice.FILTER_PROJECTILE ... });合法的FILTER_*值在 physicsdevice.ts 中定义为 physicsDevice 的属性WebGLPhysicsDevice.prototype.FILTER_DYNAMIC 1; WebGLPhysicsDevice.prototype.FILTER_STATIC 2; WebGLPhysicsDevice.prototype.FILTER_KINEMATIC 4; WebGLPhysicsDevice.prototype.FILTER_DEBRIS 8; WebGLPhysicsDevice.prototype.FILTER_TRIGGER 16; WebGLPhysicsDevice.prototype.FILTER_CHARACTER 32; WebGLPhysicsDevice.prototype.FILTER_PROJECTILE 64; WebGLPhysicsDevice.prototype.FILTER_USER_MIN 128; WebGLPhysicsDevice.prototype.FILTER_USER_MAX 0x8000; WebGLPhysicsDevice.prototype.FILTER_ALL 0xffff;4.1 回调触发背后的位标志机制在引擎内部每个接触仲裁器arbiter通过位标志contactFlags记录接触状态然后由updateContactCallbacksphysicsdevice.ts在仿真步结束时统一派发contactFlags 1→ 触发onAddedContactscontactFlags 2→ 触发onProcessedContactscontactFlags 4→ 触发onRemovedContacts派发时引擎先把仲裁器中的原始接触数据包成WebGLPhysicsPublicContact放入callbackContacts数组再分别调用物体 A、B 上注册的回调if (arbiter.contactFlags 1) { if (null ! contactCallbacksA contactCallbacksA.onAddedContacts) { contactCallbacksA.onAddedContacts(objectA._public, objectB._public, callbackContacts); } if (null ! contactCallbacksB contactCallbacksB.onAddedContacts) { contactCallbacksB.onAddedContacts(objectA._public, objectB._public, callbackContacts); } }由此可以推断一个实用结论接触回调是一对接触两端各自触发的——只要碰撞双方中的任意一方注册了回调它都会收到同一份接触数组onPreSolveContact发生在仿真步内文档明确警告若在其中执行重计算会严重拖慢性能而另外三个回调都在仿真更新结束时触发。五、实战把接触点转换到世界坐标并绘制法线Turbulenz 官方示例 physics_collisionmesh.ts 演示了 Contact 属性最典型的实战用法在onProcessedContacts回调中遍历接触数组把localPointOnB通过物体 B 的变换矩阵转换到世界空间并结合worldNormalOnB绘制接触法线线段。5.1 注册回调// samples/tsscripts/templates/physics_collisionmesh.ts //onPreSolveContact : addContact, //onAddedContacts : addContacts onProcessedContacts : addContacts //onRemovedContacts : addContacts示例中注释掉了onPreSolveContact与onAddedContacts实际启用的是onProcessedContacts。5.2 读取接触点并做本地→世界转换function addContacts(objectA, objectB, pairContacts) { if (pairContacts) { var numPairContacts pairContacts.length; objectB.calculateTransform(contactWorldTransform); for (n 0; n numPairContacts; n 1) { var pairContact pairContacts[n]; // 把本地空间的接触点 localPointOnB 变换到世界空间 mathDevice.m43TransformPoint(contactWorldTransform, pairContact.localPointOnB, contactWorldPoint); var contactNormal pairContact.worldNormalOnB; // 世界空间法线可直接使用 ... // 记录线段两端点接触点 与 接触点沿法线反方向 contact[0] contactWorldPoint[0]; contact[1] contactWorldPoint[1]; contact[2] contactWorldPoint[2]; contact[3] contactWorldPoint[0] - contactNormal[0]; contact[4] contactWorldPoint[1] - contactNormal[1]; contact[5] contactWorldPoint[2] - contactNormal[2]; numContacts 1; } } }这个片段完整展示了两个属性的正确打开方式localPointOnB必须经过变换才能用于世界空间这里调用objectB.calculateTransform(contactWorldTransform)拿到物体 B 的 4x3 变换矩阵再用mathDevice.m43TransformPoint把接触点变换为世界坐标mathdevice 相关方法见 mathdevice_api.rstworldNormalOnB本来就是世界空间法线直接参与线段计算即可无需变换收集到的世界坐标接触点随后用graphicsDevice.PRIMITIVE_LINES以线段图元渲染drawContacts函数实现接触点 法线的调试可视化。5.3 更轻量的拷贝方式如果只需要保存数据而不做可视化同样应遵循立即拷贝原则把接触信息复制到自己的对象中例如function contactsProcessed(objectA, objectB, contacts) { var n, numContacts contacts.length; for (n 0; n numContacts; n 1) { var c contacts[n]; // 立即拷贝绝不能保存 contact 引用对象会被复用 myOwnContactList.push({ localOnA : [c.localPointOnA[0], c.localPointOnA[1], c.localPointOnA[2]], localOnB : [c.localPointOnB[0], c.localPointOnB[1], c.localPointOnB[2]], normal : [c.worldNormalOnB[0], c.worldNormalOnB[1], c.worldNormalOnB[2]], isNew : c.added }); } }由于localPointOnA/B的 getter 每次调用都会通过v3Build构造一个新的Float32Array(3)直接保存返回的数组本身也是安全的它不再指向内部池数据但为了节约分配开销在回调热路径上建议只拷贝到自有复用缓冲中。六、Contact 在 2D 物理中的对应物差异提示本文档描述的是三维物理PhysicsDevice的 Contact 对象。Turbulenz 的 2D 物理Physics2DDevice采用了不同的事件模型2D 世界中接触数据封装为Physics2DContact见 physics2ddevice.ts且通过addEventListener(preSolve | begin | progress, listener)的事件监听方式分发示例见 physics2d_callbacks.ts该示例用preSolve实现单向平台、用begin/progress实现基于碰撞强度的销毁玩法。因此在阅读 API 时请注意区分3D本文主题PhysicsDevice 创建参数onPreSolveContact / onAddedContacts / onProcessedContacts / onRemovedContactsContact对象2DPhysics2DDevice 事件监听器preSolve / begin / progressPhysics2DContact对象。两者都遵循相同的核心原则接触数据生命周期极短必须立即拷贝使用。七、性能与最佳实践小结绝不持有 Contact 引用对象池复用意味着回调返回后数据随时可能被改写这是 contact_api.rst 明确强调、并由 physicsdevice.ts 的对象池注释证实的事实。警惕onPreSolveContact的代价它在仿真步内被调用文档明确警告重计算会严重拖慢物理性能需要密集读取接触数据时优先使用仿真结束时触发的onProcessedContacts。区分新接触与持续接触用added判断只在接触建立的那一次触发逻辑避免持续接触逐帧重复处理。本地坐标与世界坐标不要混用localPointOnA/B是本地空间必须用对应物体的变换矩阵转换worldNormalOnB是世界空间可直接使用。合理使用contactCallbacksMask用FILTER_*组合如 physicsdevice.ts 中定义的FILTER_PROJECTILE、FILTER_CHARACTER等过滤回调组减少无关碰撞对回调热路径的冲击。通过本文对 Contact 四个只读属性、52 维底层数据结构、对象池生命周期和回调触发机制的完整梳理开发者即可在 Turbulenz Engine 中安全、高效地实现基于接触点的各类碰撞反馈玩法。赞分享游戏开发图形学【免费下载链接】turbulenz_engineTurbulenz is a modular 3D and 2D game framework for making HTML5 powered games for browsers, desktops and mobile devices.项目地址https://gitcode.com/gh_mirrors/tu/turbulenz_engine点击查看免费下载相关推荐Godot CollisionObject2D 完全指南2D 物理碰撞对象的抽象基类、碰撞层与形状所有者机制深入解析Godot CollisionObject2D 完全指南2D 物理碰撞对象的抽象基类、碰撞层与形状所有者机制深入解析 CollisionObject2D 是文档教程游戏开发Flame 游戏引擎性能优化实战逐帧对象创建、碰撞检测与 ComponentPool 对象池Flame 游戏引擎性能优化实战逐帧对象创建、碰撞检测与 ComponentPool 对象池 导读 本文基于 Flame 官方性能指南 doc/flame/游戏开发图形学test-case版本选择指南理解MSRV策略与依赖锁定test case版本选择指南理解MSRV策略与依赖锁定 test case 是一款 Rust 过程宏属性procedural macro attribut上一篇终极指南如何快速搭建deepin-wine镜像源提升下载速度下一篇Sokol迭代器模式遍历集合创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考