- 游戏开发
- 图形学
【免费下载链接】turbulenz_engine
Turbulenz is a modular 3D and 2D game framework for making HTML5 powered games for browsers, desktops and mobile devices.
导读
Physics2DDebugDraw 是 Turbulenz 引擎中面向 2D 物理调试的高性能绘制对象:它以线(line)为基本图元、完全借助 GPU 渲染,既能直观可视化 Physics2D 世界中的刚体、碰撞形状、约束与接触点,也提供圆、曲线、螺旋、弹簧等丰富的几何原语供自定义绘制。本文以 physics2d_debugdraw_api.rst 为骨架,结合 physics2ddebugdraw.ts 源码与 physics2d.ts 等示例,完整讲解其创建方式、双视口坐标体系、全部方法与属性,以及底层的缓冲、着色器与曲线细分实现,帮助你把它无缝接入自己的物理调试流程。
概览:面向 GPU 的 2D 调试绘制
Physics2DDebugDraw 对象提供了一套快速、基于线图元、由 GPU 驱动的 2D 调试绘制 API,用于绘制 Physics2D 的物理数据。它的坐标体系与 Physics2D 世界一致,物理坐标到屏幕像素的缩放完全由设置在该对象上的两个视口(Physics2D 视口与屏幕视口)决定,这与 Draw2D 的configure视口映射思路一致。
从 physics2ddebugdraw.ts 的源码可以看到,该对象由class Physics2DDebugDraw实现并声明了static version = 1;它依赖 GraphicsDevice 在 GPU 上渲染线图元,内部使用专门的调试着色器 debugphys2d.cgfx(technique 名为alpha),支持半透明混合与裁剪。
必需脚本
与 Physics2DDevice 一样,使用 DebugDraw 需要引入两个脚本,且顺序固定(physics2ddevice 在前):
/*{{ javascript("jslib/physics2ddevice.js") }}*/ /*{{ javascript("jslib/physics2ddebugdraw.js") }}*/其中physics2ddevice.js提供 Physics2D 世界、刚体、形状、约束等核心类型,physics2ddebugdraw.js则提供 DebugDraw 对象本身。在 TypeScript 源码工程中,对应实现分别位于 physics2ddevice.ts 与 physics2ddebugdraw.ts。
创建 DebugDraw 对象
构造函数Physics2DDebugDraw.create用于创建一个 DebugDraw 对象。其核心依赖是graphicsDevice,所有线图元都通过它提交到 GPU:
var debug = Physics2DDebugDraw.create({ graphicsDevice : gd });参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
graphicsDevice | GraphicsDevice | 供 DebugDraw 渲染线图元的图形设备对象,必填 |
从 create 实现 可见,创建时会做如下初始化:
- 为屏幕视口与物理视口各分配一个
floatArray(4)端口,初始均未启用(_screenPortEnabled = false、_physics2DPortEnabled = false),并置_invalidated = true强制首次begin()重算映射; - 初始化所有绘制属性默认值(见下文“属性”一节),并预置一套完整的形状颜色表
_colors; - 通过
gd.createShader(debugphys2d_cgfx)编译内嵌的 debugphys2d.cgfx,创建clipSpace技术参数与alphatechnique; - 创建
transient类型的顶点缓冲(格式为FLOAT2 + FLOAT4,即位置 + 颜色,语义POSITION/COLOR)和索引缓冲(INDEXFORMAT_USHORT),初始容量 4 顶点、4 索引; - 内部以
Physics2DDevice.prototype.floatArray / uint16Array(详见 physics2ddevice.ts)维护 CPU 侧的顶点/索引数据数组,初始长度为 60。
双视口坐标体系:物理坐标与屏幕像素的映射
DebugDraw 通过一对视口完成“物理坐标 → 屏幕像素”的映射:setPhysics2DViewport定义物理世界中要被显示的区域,setScreenViewport定义该区域被映射到的屏幕像素区域。两个方法的效果都会在下一次调用begin()时生效。
setPhysics2DViewport
debug.setPhysics2DViewport(rectangle);rectangle:以 Physics2D 坐标为单位的矩形([x1, y1, x2, y2]),即要绘制到屏幕的物理世界区域。
如果在项目中同时使用 Draw2D,且你的图形逻辑与 Physics2D 使用同一套坐标,那么用draw2D.getViewport()的返回值作为 Physics2D 视口通常是最佳选择,可以保证调试绘制与游戏画面精确对齐。示例 physics2d.ts 的做法是:
draw2D.configure({ viewportRectangle : [0, 0, stageWidth, stageHeight], scaleMode : 'scale' }); debug.setPhysics2DViewport([0, 0, stageWidth, stageHeight]); // 30m × 22m 的物理舞台setScreenViewport
debug.setScreenViewport(rectangle);rectangle:以屏幕像素为单位、左上角为 (0,0)的矩形区域,Physics2D 视口将被映射到该区域,区域外的内容会被裁剪(clipping)。
同样地,配合 Draw2D 时,使用draw2D.getScreenSpaceViewport()是最佳实践。见 physics2d.ts:
debug.setScreenViewport(draw2D.getScreenSpaceViewport());从源码 setPhysics2DViewport/setScreenViewport 实现 可以看到:传入的矩形会被存入内部 4 元端口数组,端口启用与否由参数是否为空决定,并且每次调用都会置_invalidated = true,从而在下次begin()时重新计算缩放与裁剪矩阵。
绘制生命周期:begin、end 与 destroy
begin()
debug.begin();开始调试绘制状态。它会基于当前视口更新着色器参数(clipSpace),并在图形设备上设置裁剪矩形(scissor)。从 begin() 实现 可以看到关键细节:
- 若未显式设置屏幕视口,默认使用整个图形设备画布(
0, 0, width, height); - 若未显式设置物理视口,默认物理区域为
width/60 × height/60; - 当画布尺寸变化或视口失效时,重新计算 4 元
clipSpace参数:
clip[0] = 2 * screenW / (width * physW) clip[1] = -2 * screenH / (height * physH) clip[2] = -2 * physX * screenW / (width * physW) + 2 * screenX / width - 1 clip[3] = 2 * physY * screenH / (height * physH) - 2 * screenY / height + 1- 同时更新只读缩放因子
physics2DToScreen(取 x、y 两方向平均缩放)与screenToPhysics2D(其倒数); - 调用
gd.setScissor(...)将绘制限定在屏幕视口内,再设置 technique 与技术参数。
end()
debug.end();结束调试绘制状态,并把自匹配的begin()以来累积的所有绘制数据一次性派发(dispatch)给 GPU。对应实现end()直接调用_dispatch()(见 end/_dispatch 实现):将 CPU 侧顶点/索引数据上传到 transient 缓冲(必要时按缓冲增长算法扩容),随后setStream、setIndexBuffer并以PRIMITIVE_LINES模式执行drawIndexed,最后清零顶点/线段计数。这也是为什么 DebugDraw 能在单帧内批量提交成百上千条线而保持高效。
destroy()
debug.destroy();销毁 DebugDraw 对象,释放通过 GraphicsDevice 在 GPU 上分配的内存(顶点/索引缓冲)。对象被销毁后不可再使用。对应实现见 destroy(),它会清空曲线栈、顶点缓存与颜色表,并销毁两个 GPU 缓冲。
物理对象绘制方法
drawRigidBody
debug.drawRigidBody(rigidBody);绘制一个 RigidBody 对象,其形状的绘制颜色由本对象的各项颜色属性控制(见下文“颜色属性”)。源码实现见 drawRigidBody():先调用body._update()同步物理数据,再遍历body.shapes逐个绘制;若showBodyDetail为真,还会绘制刚体当前位置圆点以及从上一帧位置到当前位置的位移线。
drawConstraint
debug.drawConstraint(constraint);绘制一个 Constraint 对象。绘制工作委托给约束对象自身——源码中drawConstraint仅检查约束是否实现了_draw回调并调用之(见 drawConstraint())。内置约束(Pulley、Line、Distance、Angle、Weld、Point)都复用了 DebugDraw 的公共绘制方法与颜色属性,例如:
- 锚点统一用
_drawAnchor(通过多层同心圆模拟实心圆,见 tslib/physics2ddebugdraw.ts#L227-L234); - 非刚性(soft)约束的误差区域用
drawLinearSpring/drawSpiralSpring表示弹簧形变(如 Distance 约束的 _draw); - 角度类约束用
drawSpiral与_drawAngleIndicator绘制角度范围与当前角度指示(如 Angle 约束的 _draw)。
drawWorld
debug.drawWorld(world);绘制一个 World 对象。世界中哪些部分被绘制由本对象属性控制,例如showRigidBodies与showContacts。源码 drawWorld() 的逻辑为:
showRigidBodies为真时,遍历world.rigidBodies逐个调用drawRigidBody;showContacts为真时,分别调用_drawArbiters绘制动态与静态接触(arbiter)信息;showConstraints为真时,遍历world.constraints,仅绘制_active的约束。
其中接触点绘制(_drawArbiters)会用圆点标出每个接触点位置,静态接触与动态接触使用不同颜色;若showContactImpulses同时为真,还会以法向/切向两条线段可视化法向冲量与摩擦冲量。
几何原语绘制方法
所有几何原语的颜色参数均为归一化 RGBA,取值范围[0, 1];坐标参数一律使用 Physics2D 坐标。
drawLine
debug.drawLine(x1, y1, x2, y2, color);绘制单条线段。参数为起点(x1, y1)与终点(x2, y2)的物理坐标,color为归一化 RGBA。实现见 drawLine(),写入 2 个顶点(位置 + 颜色)与 1 条线段索引。
drawCurve
debug.drawCurve(x1, y1, cx, cy, x2, y2, color);绘制一条二次贝塞尔曲线:(x1, y1)为起点、(cx, cy)为控制点、(x2, y2)为终点。曲线绘制的精度由属性curveMaxError控制。实现采用迭代细分:将曲线段不断对半分解,直到曲线中点与弦中点距离小于curveMaxError换算到物理坐标的误差阈值,然后以折线逼近(见 drawCurve())。
drawRectangle
debug.drawRectangle(x1, y1, x2, y2, color);绘制轴对齐矩形,(x1, y1)为左上角、(x2, y2)为右下角(物理坐标)。实现见 drawRectangle(),写入 4 个顶点与 4 条线段。
drawCircle
debug.drawCircle(x, y, radius, color);绘制圆,(x, y)为圆心、radius为半径(物理坐标)。绘制精度由属性circleMaxError控制:实现会根据“屏幕上圆的像素半径”反推所需顶点数(ceil(π / acos(1 - maxError / rad)),最少 3 个顶点),保证弦到圆弧的最大像素误差不超过阈值(见 drawCircle())。
drawSpiral
debug.drawSpiral(x, y, angle1, angle2, radius1, radius2, color);绘制圆形螺旋(阿基米德螺旋的一段)。angle1/angle2为起止角度(顺时针弧度),radius1/radius2为起止半径(物理坐标)。实现会将螺旋按角度切成spiralMaxArc(默认 π/4 弧度)为上限的若干段,逐段用贝塞尔曲线或直线逼近(见 drawSpiral())。
drawLinearSpring
debug.drawLinearSpring(x1, y1, x2, y2, numCoils, radius, color);绘制线性弹簧(正弦波形态):(x1, y1)与(x2, y2)为弹簧两端点,numCoils为弹簧圈数(完整正弦波数,必须为正数),radius为弹簧半径(正弦波振幅,物理坐标)。实现细节(drawLinearSpring()):
- 若
numCoils <= 0或两端点距离小于minSpringLength * screenToPhysics2D阈值,则退化为直线; - 每个线圈用两条贝塞尔曲线近似(控制点取法向偏移 2 倍振幅,以保证曲线恰好达到振幅)。
drawSpiralSpring
debug.drawSpiralSpring(x, y, angle1, angle2, radius1, radius2, numCoils, color);绘制螺旋弹簧(正弦波扰动的圆形螺旋):起止角度为顺时针弧度,起止半径为物理坐标,numCoils圈数必须为正。弹簧占螺旋臂间隙的百分比由属性spiralSpringSize控制。实现与drawSpiral类似,但点的位置与梯度表达式更复杂(见 drawSpiralSpring())。
属性详解
坐标换算(只读)
| 属性 | 说明 |
|---|---|
physics2DToScreen | 物理坐标到屏幕像素的缩放因子,在每次begin()时计算(只读) |
screenToPhysics2D | 屏幕像素到物理坐标的缩放因子,begin()时计算(只读),恒等于1 / physics2DToScreen |
示例中常用debug.screenToPhysics2D把“像素尺寸”换算成物理单位,例如 physics2d_constraints.ts:
var radius = 3 * debug.screenToPhysics2D; // 3 像素 → 物理单位 debug.drawLinearSpring(posA[0], posA[1], targetX, posA[1], 3, radius, [1, 0, 0, 1]); debug.drawSpiralSpring(posB[0], posB[1], targetR, bodyB.getRotation(), radius, radius * 2, [0, 0, 1, 1]);drawWorld 显示开关
| 属性 | 默认值 | 作用 |
|---|---|---|
showConstraints | true | 是否在drawWorld(..)中绘制约束 |
showContacts | false | 是否在drawWorld(..)中绘制接触点 |
showContactImpulses | false | 是否绘制接触冲量(需同时满足showContacts为真) |
showRigidBodies | true | 是否在drawWorld(..)中绘制刚体 |
showColliderShapes | true | 是否在drawWorld(..)与drawRigidBody(..)中绘制可碰撞形状 |
showSensorShapes | true | 是否在drawWorld(..)与drawRigidBody(..)中绘制sensor为真的形状 |
showBodyDetail | false | 是否绘制刚体细节(原点、自上次更新的位移) |
showShapeDetail | false | 是否绘制形状细节(世界空间包围盒) |
示例 physics2d.ts 会按 UI 复选框动态切换:
debug.showRigidBodies = debugEnabled; debug.showContacts = contactsEnabled;值得注意:文档中该属性写作showSensorShapes,而源码 create() 与 _drawShape() 中实际字段名为showSensorsShapes,使用时以源码为准。
几何精度属性
| 属性 | 默认值 | 作用 |
|---|---|---|
circleMaxError | 0.4px | 圆的最大绘制误差,按“弦到圆弧的像素距离”度量 |
curveMaxError | 0.6px | 贝塞尔曲线的最大绘制误差,按“弦到曲线的像素距离”度量 |
spiralSpringSize | 0.75 | 螺旋弹簧占据螺旋臂间隙的比例 |
源码中的其他可调属性(文档未列出)
create() 中还初始化了更多精细控制参数,可直接读写:
- 几何类:
spiralMaxArc(默认π/4,螺旋单段最大角度)、spiralEpsilon(默认1e-5,梯度平行判定阈值)、minSpringLength(默认0.5px,弹簧最小长度阈值); - 约束类:
constraintAnchorRadius(锚点半径,默认3.0)、constraintSpringRadius(约束弹簧半径,默认3.0)、constraintSpringNumCoils(默认3)、constraintSpiralMinRadius(默认10.0)、constraintSpiralDeltaRadius(默认2.5/π)、constraintSpiralNumCoils(默认4); - 接触类:
contactRadius(接触点半径,默认3.0)、contactImpulseScale(冲量线缩放,默认30.0); - 细节类:
bodyPositionRadius(默认0.5)、circleOriginRadius(默认0.5)。
颜色属性
所有颜色均为归一化 RGBA 数组[r, g, b, a],且每个约束/接触颜色都有一组“沉睡(sleeping)”变体用于区分休眠刚体。默认值汇总如下:
| 类别 | 属性 | 默认颜色(RGBA) |
|---|---|---|
| 形状(按刚体类型) | _colors内部表 | 动态绿(0.5, 1.0, 0.5, 1.0)、静态浅红(1.0, 0.5, 0.5, 1.0)、运动学紫(0.8, 0.3, 0.8, 1.0)、子弹白(1.0, 1.0, 1.0, 1.0);sensor 变体同色系且透明度降为 0.6/0.4;sleeping 变体透明度降为 0.5 |
| 约束 | constraintColorA/B/C/D及constraintSleepingColorA/B/C/D | 红、蓝、绿、品红(alpha 0.8;sleeping 变体 alpha 0.6 且饱和度降低) |
| 约束误差 | constraintErrorColorA/B/C/D及 sleeping 变体 | 黄(1.0, 1.0, 0.5, 0.8)、青(0.5, 1.0, 1.0, 0.8)、绿、品红(alpha 0.8;sleeping 变体 alpha 0.6) |
| 接触 | dynamicContactColor | 玫红(1.0, 0.0, 0.5, 0.7) |
| 接触 | staticContactColor | 紫(0.5, 0.0, 1.0, 0.7) |
| 冲量 | normalImpulseColor/frictionImpulseColor | 纯红(1, 0, 0, 1)/ 纯蓝(0, 0, 1, 1) |
| 细节 | bodyDetailColor/shapeDetailColor | 青(0, 1, 1, 0.5)/ 黄(1, 1, 0, 0.5) |
形状颜色按type | (sleeping << 2) | (sensor << 3) | (bullet << 4)位组合索引到_colors表,见 create() 中的颜色表 与 _drawShape()。若需自定义配色,可在创建对象后直接改写上述颜色属性。
底层实现:着色器与缓冲策略
调试着色器
DebugDraw 使用专用着色器 debugphys2d.cgfx:
float4 clipSpace; void vp_draw2dlines(in float2 inpos : POSITION, in float4 incol : COLOR, out float4 outpos : TZ_OUT_POSITION, out float4 outcol : COLOR) { outpos = float4(inpos * clipSpace.xy + clipSpace.zw, 0, 1); outcol = incol; }顶点程序把物理坐标通过clipSpace参数直接变换到裁剪空间;alphatechnique 关闭深度测试与背面剔除,开启SrcAlpha, OneMinusSrcAlpha混合,因此调试线天然支持半透明叠加显示。
动态缓冲增长
为了在“省内存”与“少扩容”之间取得平衡,_bufferSizeAlgorithm 采用 1.25 增长因子:size = 6 * ceil(1.25^ceil(log1.25(target)) / 6),保证缓冲区大小始终是顶点步长(6,即 FLOAT2 位置 + FLOAT4 颜色)的整数倍,避免产生无法利用的字节。每次_dispatch()前若当前计数超出缓冲容量,会销毁旧缓冲并按该算法重建,再一次性上传。
实战:接入主循环的完整模式
综合 physics2d.ts 与 physics2d_constraints.ts 两个示例,典型接入流程如下:
1. 初始化阶段:创建 GraphicsDevice 后,创建 Draw2D 与 DebugDraw,并把两者的视口统一到物理舞台:
var phys2D = Physics2DDevice.create(); var stageWidth = 30, stageHeight = 22; // 单位:米 var draw2D = Draw2D.create({ graphicsDevice : graphicsDevice }); var debug = Physics2DDebugDraw.create({ graphicsDevice : graphicsDevice }); draw2D.configure({ viewportRectangle : [0, 0, stageWidth, stageHeight], scaleMode : 'scale' }); debug.setPhysics2DViewport([0, 0, stageWidth, stageHeight]);2. 主循环渲染:每帧先推进物理模拟,再在begin()/end()之间提交绘制:
world.step(1 / 60); debug.setScreenViewport(draw2D.getScreenSpaceViewport()); debug.showRigidBodies = debugEnabled; debug.showContacts = contactsEnabled; debug.begin(); debug.drawWorld(world); // 或按需 drawRigidBody / drawConstraint debug.end(); graphicsDevice.endFrame();3. 扩展自定义约束的调试绘制:内置约束把绘制委托给自身的_draw,自定义约束同样可以通过debugDraw回调接入,见 physics2d_constraints.ts 中的自定义约束:
debugDraw : function debugDrawFn(debug, stiff) { if (stiff) { return; } var bodyA = this.bodies[0]; var bodyB = this.bodies[1]; var posA = bodyA.getPosition(); var posB = bodyB.getPosition(); var targetX = ((bodyB.getRotation()) / (Math.PI / 5)) + 35; var targetR = (Math.PI / 5 * (posA[0] - 35)); // 3 像素弹簧半径 var radius = 3 * debug.screenToPhysics2D; debug.drawLinearSpring(posA[0], posA[1], targetX, posA[1], 3, radius, [1, 0, 0, 1]); debug.drawSpiralSpring(posB[0], posB[1], targetR, bodyB.getRotation(), radius, radius * 2, [0, 0, 1, 1]); }总结
Physics2DDebugDraw 把“调试可视化”做成了与游戏渲染同等的 GPU 流水线:通过setPhysics2DViewport/setScreenViewport双视口实现物理坐标到屏幕像素的精确映射,begin()/end()之间批量累积线图元、一次提交,配合可调精度的圆、曲线、螺旋、弹簧原语与完备的颜色体系,可以直观呈现刚体形状、约束误差、接触点与冲量等物理细节。相关示例(physics2d.ts、physics2d_constraints.ts、physics2d_callbacks.ts)与 API 文档(physics2d_debugdraw_api.rst)可作为继续深入与直接复用的参考。
- 游戏开发
- 图形学
【免费下载链接】turbulenz_engine
Turbulenz is a modular 3D and 2D game framework for making HTML5 powered games for browsers, desktops and mobile devices.
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考