import { _decorator, Camera, CCObject, Color, Component, director, geometry, GeometryRenderer, gfx, Layers, Mat4, Material, Node, toRadian, Vec3, } from 'cc'; import { cce, Editor } from './Define'; const { ccclass, executeInEditMode, property } = _decorator; class GizmosRenderer { private _geometryRenderer; private _drawCalls: ((geometry) => void)[] = []; constructor(geometryRenderer: GeometryRenderer) { this._geometryRenderer = geometryRenderer; } public addDrawCall(drawCall: (geometry: GeometryRenderer) => void) { this._drawCalls.push(drawCall); } public clear() { this._drawCalls = []; } public draw() { this._drawCalls.forEach((drawCall) => { drawCall(this._geometryRenderer); }); } } @ccclass('Gizmos3D.GizmosDebugDraw') @executeInEditMode class GizmosDebugDraw extends Component { private _renderers: Map = new Map(); private _color: Color = Gizmos3D.DEFAULT_COLOR; private _renderer: GeometryRenderer = null; private _depthTest: boolean = false; private _useLocalPosition: boolean = false; private _parentNode: Node; protected onLoad(): void { if (cce) { cce.Camera._camera.camera.initGeometryRenderer(); this._renderer = cce.Camera._camera.camera.geometryRenderer; } else { const camera = director.getScene().getComponentInChildren(Camera).camera; camera.initGeometryRenderer(); this._renderer = camera.geometryRenderer; } if (!this._renderer) { console.warn( 'Unable to initialize geometryRenderer for Gizmos3D, please ensure the Geometry Renderer feature is enabled in project settings if you want to use Gizmos3D in play mode', ); } } protected update(dt: number): void { //only call in editor this.callNodeDrawGizmos(); this._renderers.forEach((renderer) => { renderer.draw(); renderer.clear(); }); this._color = Gizmos3D.DEFAULT_COLOR; this._useLocalPosition = false; } private callNodeDrawGizmos() { if (Editor && this._parentNode) { const selectedList: string[] = Editor.Selection.getSelected('node'); if (selectedList.includes(this._parentNode.uuid)) { const comps = this._parentNode.components; for (let i = 0; i < comps.length; i++) { const comp: Component = comps[i]; comp.onDrawGizmos?.(); } } } } private createRenderer(color: Color) { const renderer = new GizmosRenderer(this._renderer); return renderer; } private getRenderer(color: Color): GizmosRenderer { const hex = color.toHEX(); let renderer = this._renderers.get(hex); if (!renderer) { renderer = this.createRenderer(color); this._renderers.set(hex, renderer); } return renderer; } private worldToLocal(world: Vec3): Vec3 { const local = new Vec3(); Vec3.add(local, this.node.worldPosition, world); return local; } public registerDrawGizmos(node: Node) { this._parentNode = node; } public setDepthTest(value: boolean) { this._depthTest = value; } public setColor(color: Color) { this._color = color; } public setUseLocalPosition(value: boolean) { this._useLocalPosition = value; } public clearAll() { this._renderers.forEach((renderer) => renderer.clear()); } private rotate(pos: Vec3, rot: Vec3 = Vec3.ZERO): Mat4 { let result = new Mat4(); let transform = new Mat4(); Mat4.fromTranslation(result, pos); Mat4.fromXRotation(transform, toRadian(rot.x)); result.multiply(transform); Mat4.fromYRotation(transform, toRadian(rot.y)); result.multiply(transform); Mat4.fromZRotation(transform, toRadian(rot.z)); result.multiply(transform); Mat4.fromTranslation(transform, new Vec3(-pos.x, -pos.y, -pos.z)); result.multiply(transform); return result; } public drawLine(point1: Vec3, point2: Vec3) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p1 = this._useLocalPosition ? this.worldToLocal(point1) : point1; const p2 = this._useLocalPosition ? this.worldToLocal(point2) : point2; renderer.addDrawCall((geometry) => { geometry?.addLine(p1, p2, color, this._depthTest); }); } public drawLineList(points: Vec3[], close: boolean = false) { const color = this._color.clone(); const renderer = this.getRenderer(color); const pointList = this._useLocalPosition ? points.map((p) => this.worldToLocal(p)) : points; renderer.addDrawCall((geometry) => { if (pointList.length > 0) { for (let i = 0; i < pointList.length - 1; i++) { geometry?.addLine(pointList[i], pointList[i + 1], color, this._depthTest); } if (close) { geometry?.addLine(pointList[pointList.length - 1], pointList[0], color, this._depthTest); } } }); } public drawDashLine(point1: Vec3, point2: Vec3) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p1 = this._useLocalPosition ? this.worldToLocal(point1) : point1; const p2 = this._useLocalPosition ? this.worldToLocal(point2) : point2; renderer.addDrawCall((geometry) => { geometry?.addDashedLine(p1, p2, color, this._depthTest); }); } public drawDashLineList(points: Vec3[], close: boolean = false) { const color = this._color.clone(); const renderer = this.getRenderer(color); const pointList = this._useLocalPosition ? points.map((p) => this.worldToLocal(p)) : points; renderer.addDrawCall((geometry) => { if (pointList.length > 0) { for (let i = 0; i < pointList.length - 1; i++) { geometry?.addDashedLine(pointList[i], pointList[i + 1], color, this._depthTest); } if (close) { geometry?.addDashedLine(pointList[pointList.length - 1], pointList[0], color, this._depthTest); } } }); } public drawCircle(center: Vec3, radius: number, rot: Vec3 = Vec3.ZERO) { const color = this._color.clone(); const renderer = this.getRenderer(color); const c = this._useLocalPosition ? this.worldToLocal(center) : center; const transform = this.rotate(c, rot); renderer.addDrawCall((geometry) => { geometry?.addCircle(c, radius, color, 32, this._depthTest, true, transform); }); } public drawDisc(center: Vec3, radius: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false) { const color = this._color.clone(); const renderer = this.getRenderer(color); const c = this._useLocalPosition ? this.worldToLocal(center) : center; const transform = this.rotate(c, rot); renderer.addDrawCall((geometry) => { geometry?.addDisc(c, radius, color, 32, wireFrame, this._depthTest, true, true, transform); }); } public drawQuad(point1: Vec3, point2: Vec3, point3: Vec3, point4: Vec3, wireFrame: boolean = false) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p1 = this._useLocalPosition ? this.worldToLocal(point1) : point1; const p2 = this._useLocalPosition ? this.worldToLocal(point2) : point2; const p3 = this._useLocalPosition ? this.worldToLocal(point3) : point3; const p4 = this._useLocalPosition ? this.worldToLocal(point4) : point4; renderer.addDrawCall((geometry) => { geometry?.addQuad(p1, p2, p3, p4, color, wireFrame, this._depthTest); }); } public drawSphere(center: Vec3, radius: number, wireFrame: boolean = false) { const color = this._color.clone(); const renderer = this.getRenderer(color); const c = this._useLocalPosition ? this.worldToLocal(center) : center; renderer.addDrawCall((geometry) => { geometry?.addSphere(c, radius, color, 32, 16, wireFrame, this._depthTest); }); } public drawArc(center: Vec3, radius: number, startAngle: number, endAngle: number, rot: Vec3 = Vec3.ZERO) { const color = this._color.clone(); const renderer = this.getRenderer(color); const c = this._useLocalPosition ? this.worldToLocal(center) : center; const transform = this.rotate(c, rot); renderer.addDrawCall((geometry) => { geometry?.addArc(c, radius, color, startAngle, endAngle, 32, this._depthTest, true, transform); }); } public drawSolidArc( center: Vec3, radius: number, startAngle: number, endAngle: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const color = this._color.clone(); const renderer = this.getRenderer(color); const c = this._useLocalPosition ? this.worldToLocal(center) : center; const transform = this.rotate(c, rot); renderer.addDrawCall((geometry) => { geometry?.addSector( c, radius, color, startAngle, endAngle, 32, wireFrame, this._depthTest, true, true, transform, ); }); } public drawPolygon( position: Vec3, radius: number, segments: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p = this._useLocalPosition ? this.worldToLocal(position) : position; const transform = this.rotate(p, rot); renderer.addDrawCall((geometry) => { geometry?.addPolygon(p, radius, color, segments, wireFrame, this._depthTest, true, true, transform); }); } public drawOctahedron(position: Vec3, radius: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p = this._useLocalPosition ? this.worldToLocal(position) : position; const transform = this.rotate(p, rot); renderer.addDrawCall((geometry) => { geometry?.addOctahedron(p, radius, color, wireFrame, this._depthTest, false, true, transform); }); } public drawCross(position: Vec3, size: number) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p = this._useLocalPosition ? this.worldToLocal(position) : position; renderer.addDrawCall((geometry) => { geometry?.addCross(p, size, color, this._depthTest); }); } public drawCapsule( position: Vec3, radius: number, height: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p = this._useLocalPosition ? this.worldToLocal(position) : position; const transform = this.rotate(p, rot); renderer.addDrawCall((geometry) => { geometry?.addCapsule(p, radius, height, color, 32, 8, wireFrame, this._depthTest, false, true, transform); }); } public drawBox(position: Vec3, size: Vec3, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p = this._useLocalPosition ? this.worldToLocal(position) : position; const transform = this.rotate(p, rot); let box = geometry?.AABB.create(p.x, p.y, p.z, size.x, size.y, size.z); renderer.addDrawCall((geometry) => { geometry?.addBoundingBox(box, color, wireFrame, this._depthTest, false, true, transform); }); } public drawCylinder( position: Vec3, radius: number, height: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p = this._useLocalPosition ? this.worldToLocal(position) : position; const transform = this.rotate(p, rot); renderer.addDrawCall((geometry) => { geometry?.addCylinder(p, radius, height, color, 32, wireFrame, this._depthTest, false, true, transform); }); } public drawCone(position: Vec3, radius: number, height: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p = this._useLocalPosition ? this.worldToLocal(position) : position; const transform = this.rotate(p, rot); renderer.addDrawCall((geometry) => { geometry?.addCone(p, radius, height, color, 32, wireFrame, this._depthTest, false, true, transform); }); } public drawBezier(point1: Vec3, point2: Vec3, point3: Vec3, point4: Vec3, rot: Vec3 = Vec3.ZERO) { const color = this._color.clone(); const renderer = this.getRenderer(color); const p1 = this._useLocalPosition ? this.worldToLocal(point1) : point1; const p2 = this._useLocalPosition ? this.worldToLocal(point2) : point2; const p3 = this._useLocalPosition ? this.worldToLocal(point3) : point3; const p4 = this._useLocalPosition ? this.worldToLocal(point4) : point4; const transform = this.rotate(p1, rot); renderer.addDrawCall((geometry) => { geometry?.addBezier(p1, p2, p3, p4, color, 32, this._depthTest, true, transform); }); } public drawSpline(knots: Vec3[], mode: geometry.SplineMode = geometry?.SplineMode.BEZIER, knotSize = 0.5) { const color = this._color.clone(); const renderer = this.getRenderer(color); const knotsList = this._useLocalPosition ? knots.map((knot) => this.worldToLocal(knot)) : knots; let spline = geometry?.Spline.create(mode, knotsList); renderer.addDrawCall((geometry) => { geometry?.addSpline(spline, color, 0xffffffff, knotSize, 32, this._depthTest); }); } } export default class Gizmos3D { private static _mat: Material; public static get DEFAULT_MAT() { if (!this._mat) { this._mat = new Material(); this._mat.initialize({ effectName: 'builtin-unlit', defines: { USE_VERTEX_COLOR: true }, states: { primitive: gfx.PrimitiveMode.LINE_LOOP }, }); this._mat.passes.forEach((v) => v.tryCompile()); } return this._mat; } public static readonly DEFAULT_COLOR = Color.BLUE; public static readonly DEFAULT_LAYER = Layers.Enum.GIZMOS; private static getDebugNode(node: Node) { let debugNode = node.getComponentInChildren(GizmosDebugDraw); if (!debugNode) { debugNode = new Node('DEBUG_DRAW_NODE').addComponent(GizmosDebugDraw); debugNode.node.layer = this.DEFAULT_LAYER; debugNode.node.hideFlags |= CCObject.Flags.DontSave | CCObject.Flags.HideInHierarchy; debugNode.node.parent = node; } return debugNode; } public static registerDrawGizmos(node: Node) { this.getDebugNode(node).registerDrawGizmos(node); } public static beginColor(node: Node, color: Color) { this.getDebugNode(node).setColor(color); } static beginLocalPosition(node: Node) { this.getDebugNode(node).setUseLocalPosition(true); } static endLocalPosition(node: Node) { this.getDebugNode(node).setUseLocalPosition(false); } public static clearAll(node: Node) { this.getDebugNode(node).clearAll(); } public static drawLine(node: Node, point1: Vec3, point2: Vec3) { const debugNode = this.getDebugNode(node); debugNode.drawLine(point1, point2); } public static drawLineList(node: Node, points: Vec3[], close: boolean = false) { const debugNode = this.getDebugNode(node); debugNode.drawLineList(points, close); } public static drawDashLine(node: Node, point1: Vec3, point2: Vec3) { const debugNode = this.getDebugNode(node); debugNode.drawDashLine(point1, point2); } public static drawDashLineList(node: Node, points: Vec3[], close: boolean = false) { const debugNode = this.getDebugNode(node); debugNode.drawLineList(points, close); } public static drawCircle(node: Node, center: Vec3, radius: number, rot: Vec3 = Vec3.ZERO) { const debugNode = this.getDebugNode(node); debugNode.drawCircle(center, radius, rot); } public static drawDisc( node: Node, center: Vec3, radius: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const debugNode = this.getDebugNode(node); debugNode.drawDisc(center, radius, rot, wireFrame); } public static drawQuad( node: Node, point1: Vec3, point2: Vec3, point3: Vec3, point4: Vec3, wireFrame: boolean = false, ) { const debugNode = this.getDebugNode(node); debugNode.drawQuad(point1, point2, point3, point4, wireFrame); } public static drawSphere(node: Node, center: Vec3, radius: number, wireFrame: boolean = false) { const debugNode = this.getDebugNode(node); debugNode.drawSphere(center, radius, wireFrame); } public static drawArc( node: Node, center: Vec3, radius: number, startAngle: number, endAngle: number, rot: Vec3 = Vec3.ZERO, ) { const debugNode = this.getDebugNode(node); debugNode.drawArc(center, radius, startAngle, endAngle, rot); } public static drawSolidArc( node: Node, center: Vec3, radius: number, startAngle: number, endAngle: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const debugNode = this.getDebugNode(node); debugNode.drawSolidArc(center, radius, startAngle, endAngle, rot, wireFrame); } public static drawPolygon( node: Node, position: Vec3, radius: number, segments: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const debugNode = this.getDebugNode(node); debugNode.drawPolygon(position, radius, segments, rot, wireFrame); } public static drawOctahedron( node: Node, position: Vec3, radius: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const debugNode = this.getDebugNode(node); debugNode.drawOctahedron(position, radius, rot, wireFrame); } public static drawCross(node: Node, center: Vec3, size: number) { const debugNode = this.getDebugNode(node); debugNode.drawCross(center, size); } public static drawCapsule( node: Node, position: Vec3, radius: number, height: number, rot?: Vec3, wireFrame?: boolean, ) { const debugNode = this.getDebugNode(node); debugNode.drawCapsule(position, radius, height, rot, wireFrame); } public static drawBox(node: Node, center: Vec3, size: Vec3, rot?: Vec3, wireFrame?: boolean) { const debugNode = this.getDebugNode(node); debugNode.drawBox(center, size, rot, wireFrame); } public static drawCylinder( node: Node, position: Vec3, radius: number, height: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const debugNode = this.getDebugNode(node); debugNode.drawCylinder(position, radius, height, rot, wireFrame); } public static drawCone( node: Node, position: Vec3, radius: number, height: number, rot: Vec3 = Vec3.ZERO, wireFrame: boolean = false, ) { const debugNode = this.getDebugNode(node); debugNode.drawCone(position, radius, height, rot, wireFrame); } public static drawBezier(node: Node, point1: Vec3, point2: Vec3, point3: Vec3, point4: Vec3, rot?: Vec3) { const debugNode = this.getDebugNode(node); debugNode.drawBezier(point1, point2, point3, point4, rot); } public static drawSpline( node: Node, knots: Vec3[], mode: geometry.SplineMode = geometry?.SplineMode.BEZIER, knotSize = 0.5, ) { const debugNode = this.getDebugNode(node); debugNode.drawSpline(knots, mode, knotSize); } }