☰
Turbulenz Physics2DDebugDraw 调试绘制 API 详解:GPU 线绘制与物理数据可视化
2026/10/14 15:42:14 网站建设 项目流程
  • 游戏开发
  • 图形学

【免费下载链接】turbulenz_engine

Turbulenz 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
点击查看免费下载

导读

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 });

参数说明:

参数类型说明
graphicsDeviceGraphicsDevice供 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() 的逻辑为:

  1. showRigidBodies为真时,遍历world.rigidBodies逐个调用drawRigidBody;
  2. showContacts为真时,分别调用_drawArbiters绘制动态与静态接触(arbiter)信息;
  3. 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 显示开关

属性默认值作用
showConstraintstrue是否在drawWorld(..)中绘制约束
showContactsfalse是否在drawWorld(..)中绘制接触点
showContactImpulsesfalse是否绘制接触冲量(需同时满足showContacts为真)
showRigidBodiestrue是否在drawWorld(..)中绘制刚体
showColliderShapestrue是否在drawWorld(..)与drawRigidBody(..)中绘制可碰撞形状
showSensorShapestrue是否在drawWorld(..)与drawRigidBody(..)中绘制sensor为真的形状
showBodyDetailfalse是否绘制刚体细节(原点、自上次更新的位移)
showShapeDetailfalse是否绘制形状细节(世界空间包围盒)

示例 physics2d.ts 会按 UI 复选框动态切换:

debug.showRigidBodies = debugEnabled; debug.showContacts = contactsEnabled;

值得注意:文档中该属性写作showSensorShapes,而源码 create() 与 _drawShape() 中实际字段名为showSensorsShapes,使用时以源码为准。

几何精度属性

属性默认值作用
circleMaxError0.4px圆的最大绘制误差,按“弦到圆弧的像素距离”度量
curveMaxError0.6px贝塞尔曲线的最大绘制误差,按“弦到曲线的像素距离”度量
spiralSpringSize0.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.

项目地址:https://gitcode.com/gh_mirrors/tu/turbulenz_engine
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询