package armory.trait.internal; import iron.Trait; #if arm_debug import kha.Scheduler; import iron.object.CameraObject; import iron.object.MeshObject; import zui.Zui; import zui.Id; using armory.object.TransformExtension; #end #if arm_debug @:access(zui.Zui) @:access(armory.logicnode.LogicNode) #end class DebugConsole extends Trait { #if (!arm_debug) public function new() { super(); } #else public var visible = true; var ui: Zui; var scaleFactor = 1.0; var lastTime = 0.0; var frameTime = 0.0; var totalTime = 0.0; var frames = 0; var frameTimeAvg = 0.0; var frameTimeAvgMin = 0.0; var frameTimeAvgMax = 0.0; var renderPathTime = 0.0; var renderPathTimeAvg = 0.0; var updateTime = 0.0; var updateTimeAvg = 0.0; var animTime = 0.0; var animTimeAvg = 0.0; var physTime = 0.0; var physTimeAvg = 0.0; var graph: kha.Image = null; var graphA: kha.Image = null; var graphB: kha.Image = null; var benchmark = false; var benchFrames = 0; var benchTime = 0.0; var selectedObject: iron.object.Object; var selectedType = ""; var selectedTraits = new Array(); static var lrow = [1 / 2, 1 / 2]; static var row4 = [1 / 4, 1 / 4, 1 / 4, 1 / 4]; public static var debugFloat = 1.0; public static var watchNodes: Array = []; public function new(scaleFactor = 1.0) { super(); this.scaleFactor = scaleFactor; iron.data.Data.getFont("font_default.ttf", function(font: kha.Font) { ui = new Zui({scaleFactor: scaleFactor, font: font}); notifyOnRender2D(render2D); notifyOnUpdate(update); if (haxeTrace == null) { haxeTrace = haxe.Log.trace; haxe.Log.trace = consoleTrace; } // Toggle console kha.input.Keyboard.get().notify(null, null, function(char: String) { if (char == "~") visible = !visible; else if (char == "[") { debugFloat -= 0.1; trace(debugFloat); } else if (char == "]") { debugFloat += 0.1; trace(debugFloat); } }); }); } var debugDrawSet = false; function selectObject(o: iron.object.Object) { selectedObject = o; if (!debugDrawSet) { debugDrawSet = true; armory.trait.internal.DebugDraw.notifyOnRender(function(draw: armory.trait.internal.DebugDraw) { if (selectedObject != null) draw.bounds(selectedObject.transform); }); } } function updateGraph() { if (graph == null) { graphA = kha.Image.createRenderTarget(280, 33); graphB = kha.Image.createRenderTarget(280, 33); graph = graphA; } else graph = graph == graphA ? graphB : graphA; var graphPrev = graph == graphA ? graphB : graphA; graph.g2.begin(true, 0x00000000); graph.g2.color = 0xffffffff; graph.g2.drawImage(graphPrev, -3, 0); var avg = Math.round(frameTimeAvg * 1000); var miss = avg > 16.7 ? (avg - 16.7) / 16.7 : 0.0; graph.g2.color = kha.Color.fromFloats(miss, 1 - miss, 0, 1.0); graph.g2.fillRect(280 - 3, 33 - avg, 3, avg); graph.g2.color = 0xff000000; graph.g2.fillRect(280 - 3, 33 - 17, 3, 1); graph.g2.end(); } static var haxeTrace: Dynamic->haxe.PosInfos->Void = null; static var lastTraces: Array = [""]; static function consoleTrace(v: Dynamic, ?inf: haxe.PosInfos) { lastTraces.unshift(haxe.Log.formatOutput(v,inf)); if (lastTraces.length > 10) lastTraces.pop(); haxeTrace(v, inf); } function render2D(g: kha.graphics2.Graphics) { if (!visible) return; var hwin = Id.handle(); var htab = Id.handle({position: 0}); var ww = Std.int(280 * scaleFactor); var wx = iron.App.w() - ww; var wy = 0; var wh = iron.App.h(); // var bindG = ui.windowDirty(hwin, wx, wy, ww, wh) || hwin.redraws > 0; var bindG = true; if (bindG) g.end(); ui.begin(g); if (ui.window(hwin, wx, wy, ww, wh, true)) { if (ui.tab(htab, "")) {} if (ui.tab(htab, "Scene")) { if (ui.panel(Id.handle({selected: true}), "Outliner")) { ui.indent(); var lineCounter = 0; function drawList(listHandle: zui.Zui.Handle, currentObject: iron.object.Object) { if (currentObject.name.charAt(0) == ".") return; // Hidden var b = false; // Highlight every other line if (lineCounter % 2 == 0) { ui.g.color = ui.t.SEPARATOR_COL; ui.g.fillRect(0, ui._y, ui._windowW, ui.ELEMENT_H()); ui.g.color = 0xffffffff; } // Highlight selected line if (currentObject == selectedObject) { ui.g.color = 0xff205d9c; ui.g.fillRect(0, ui._y, ui._windowW, ui.ELEMENT_H()); ui.g.color = 0xffffffff; } if (currentObject.children.length > 0) { ui.row([1 / 13, 12 / 13]); b = ui.panel(listHandle.nest(lineCounter, {selected: true}), "", true, false, false); ui.text(currentObject.name); } else { ui._x += 18; // Sign offset // Draw line that shows parent relations ui.g.color = ui.t.ACCENT_COL; ui.g.drawLine(ui._x - 10, ui._y + ui.ELEMENT_H() / 2, ui._x, ui._y + ui.ELEMENT_H() / 2); ui.g.color = 0xffffffff; ui.text(currentObject.name); ui._x -= 18; } lineCounter++; // Undo applied offset for row drawing caused by endElement() in Zui.hx ui._y -= ui.ELEMENT_OFFSET(); if (ui.isReleased) { selectObject(currentObject); } if (b) { var currentY = ui._y; for (child in currentObject.children) { ui.indent(); drawList(listHandle, child); ui.unindent(); } // Draw line that shows parent relations ui.g.color = ui.t.ACCENT_COL; ui.g.drawLine(ui._x + 14, currentY, ui._x + 14, ui._y - ui.ELEMENT_H() / 2); ui.g.color = 0xffffffff; } } for (c in iron.Scene.active.root.children) { drawList(Id.handle(), c); } ui.unindent(); } if (selectedObject == null) selectedType = ""; if (ui.panel(Id.handle({selected: true}), 'Properties $selectedType')) { ui.indent(); if (selectedObject != null) { var h = Id.handle(); h.selected = selectedObject.visible; selectedObject.visible = ui.check(h, "Visible"); var localPos = selectedObject.transform.loc; var worldPos = selectedObject.transform.getWorldPosition(); var scale = selectedObject.transform.scale; var rot = selectedObject.transform.rot.getEuler(); var dim = selectedObject.transform.dim; rot.mult(180 / 3.141592); var f = 0.0; ui.text("Transforms"); ui.indent(); ui.row(row4); ui.text("World Loc"); // Read-only currently ui.enabled = false; h = Id.handle(); h.text = roundfp(worldPos.x) + ""; f = Std.parseFloat(ui.textInput(h, "X")); h = Id.handle(); h.text = roundfp(worldPos.y) + ""; f = Std.parseFloat(ui.textInput(h, "Y")); h = Id.handle(); h.text = roundfp(worldPos.z) + ""; f = Std.parseFloat(ui.textInput(h, "Z")); ui.enabled = true; ui.row(row4); ui.text("Local Loc"); h = Id.handle(); h.text = roundfp(localPos.x) + ""; f = Std.parseFloat(ui.textInput(h, "X")); if (ui.changed) localPos.x = f; h = Id.handle(); h.text = roundfp(localPos.y) + ""; f = Std.parseFloat(ui.textInput(h, "Y")); if (ui.changed) localPos.y = f; h = Id.handle(); h.text = roundfp(localPos.z) + ""; f = Std.parseFloat(ui.textInput(h, "Z")); if (ui.changed) localPos.z = f; ui.row(row4); ui.text("Rotation"); h = Id.handle(); h.text = roundfp(rot.x) + ""; f = Std.parseFloat(ui.textInput(h, "X")); var changed = false; if (ui.changed) { changed = true; rot.x = f; } h = Id.handle(); h.text = roundfp(rot.y) + ""; f = Std.parseFloat(ui.textInput(h, "Y")); if (ui.changed) { changed = true; rot.y = f; } h = Id.handle(); h.text = roundfp(rot.z) + ""; f = Std.parseFloat(ui.textInput(h, "Z")); if (ui.changed) { changed = true; rot.z = f; } if (changed && selectedObject.name != "Scene") { rot.mult(3.141592 / 180); selectedObject.transform.rot.fromEuler(rot.x, rot.y, rot.z); selectedObject.transform.buildMatrix(); #if arm_physics var rb = selectedObject.getTrait(armory.trait.physics.RigidBody); if (rb != null) rb.syncTransform(); #end } ui.row(row4); ui.text("Scale"); h = Id.handle(); h.text = roundfp(scale.x) + ""; f = Std.parseFloat(ui.textInput(h, "X")); if (ui.changed) scale.x = f; h = Id.handle(); h.text = roundfp(scale.y) + ""; f = Std.parseFloat(ui.textInput(h, "Y")); if (ui.changed) scale.y = f; h = Id.handle(); h.text = roundfp(scale.z) + ""; f = Std.parseFloat(ui.textInput(h, "Z")); if (ui.changed) scale.z = f; ui.row(row4); ui.text("Dimensions"); h = Id.handle(); h.text = roundfp(dim.x) + ""; f = Std.parseFloat(ui.textInput(h, "X")); if (ui.changed) dim.x = f; h = Id.handle(); h.text = roundfp(dim.y) + ""; f = Std.parseFloat(ui.textInput(h, "Y")); if (ui.changed) dim.y = f; h = Id.handle(); h.text = roundfp(dim.z) + ""; f = Std.parseFloat(ui.textInput(h, "Z")); if (ui.changed) dim.z = f; selectedObject.transform.dirty = true; ui.unindent(); if (selectedObject.traits.length > 0) { ui.text("Traits:"); ui.indent(); for (t in selectedObject.traits) { ui.row([3/4, 1/4]); ui.text(Type.getClassName(Type.getClass(t))); if (ui.button("Details")) { if (selectedTraits.indexOf(t) == -1) { selectedTraits.push(t); } } } ui.unindent(); } if (selectedObject.name == "Scene") { selectedType = "(Scene)"; if (iron.Scene.active.world != null) { var p = iron.Scene.active.world.probe; p.raw.strength = ui.slider(Id.handle({value: p.raw.strength}), "Env Strength", 0.0, 5.0, true); } else { ui.text("This scene has no world data to edit."); } } else if (Std.is(selectedObject, iron.object.LightObject)) { selectedType = "(Light)"; var light = cast(selectedObject, iron.object.LightObject); var lightHandle = Id.handle(); lightHandle.value = light.data.raw.strength / 10; light.data.raw.strength = ui.slider(lightHandle, "Strength", 0.0, 5.0, true) * 10; } else if (Std.is(selectedObject, iron.object.CameraObject)) { selectedType = "(Camera)"; var cam = cast(selectedObject, iron.object.CameraObject); var fovHandle = Id.handle(); fovHandle.value = Std.int(cam.data.raw.fov * 100) / 100; cam.data.raw.fov = ui.slider(fovHandle, "Field of View", 0.3, 2.0, true); if (ui.changed) { cam.buildProjection(); } } else { selectedType = "(Object)"; } } ui.unindent(); } } var avg = Math.round(frameTimeAvg * 10000) / 10; var fpsAvg = avg > 0 ? Math.round(1000 / avg) : 0; if (ui.tab(htab, '$avg ms')) { if (ui.panel(Id.handle({selected: true}), "Performance")) { if (graph != null) ui.image(graph); ui.indent(); ui.row(lrow); ui.text("Frame"); ui.text('$avg ms / $fpsAvg fps', Align.Right); ui.row(lrow); ui.text("Render-path"); ui.text(Math.round(renderPathTimeAvg * 10000) / 10 + " ms", Align.Right); ui.row(lrow); ui.text("Script"); ui.text(Math.round((updateTimeAvg - physTimeAvg - animTimeAvg) * 10000) / 10 + " ms", Align.Right); ui.row(lrow); ui.text("Animation"); ui.text(Math.round(animTimeAvg * 10000) / 10 + " ms", Align.Right); ui.row(lrow); ui.text("Physics"); ui.text(Math.round(physTimeAvg * 10000) / 10 + " ms", Align.Right); ui.unindent(); } if (ui.panel(Id.handle({selected: false}), "Draw")) { ui.indent(); ui.row(lrow); var numMeshes = iron.Scene.active.meshes.length; ui.text("Meshes"); ui.text(numMeshes + "", Align.Right); ui.row(lrow); ui.text("Draw calls"); ui.text(iron.RenderPath.drawCalls + "", Align.Right); ui.row(lrow); ui.text("Tris mesh"); ui.text(iron.RenderPath.numTrisMesh + "", Align.Right); ui.row(lrow); ui.text("Tris shadow"); ui.text(iron.RenderPath.numTrisShadow + "", Align.Right); #if arm_batch ui.row(lrow); ui.text("Batch calls"); ui.text(iron.RenderPath.batchCalls + "", Align.Right); ui.row(lrow); ui.text("Batch buckets"); ui.text(iron.RenderPath.batchBuckets + "", Align.Right); #end ui.row(lrow); ui.text("Culled"); // Assumes shadow context for all meshes ui.text(iron.RenderPath.culled + " / " + numMeshes * 2, Align.Right); #if arm_stream ui.row(lrow); var total = iron.Scene.active.sceneStream.sceneTotal(); ui.text("Streamed"); ui.text('$numMeshes / $total', Align.Right); #end ui.unindent(); } if (ui.panel(Id.handle({selected: false}), "Render Targets")) { ui.indent(); #if (kha_opengl || kha_webgl) ui.imageInvertY = true; #end for (rt in iron.RenderPath.active.renderTargets) { ui.text(rt.raw.name); if (rt.image != null && !rt.is3D) { ui.image(rt.image); } } #if (kha_opengl || kha_webgl) ui.imageInvertY = false; #end ui.unindent(); } if (ui.panel(Id.handle({selected: false}), "Cached Materials")) { ui.indent(); for (c in iron.data.Data.cachedMaterials) { ui.text(c.name); } ui.unindent(); } if (ui.panel(Id.handle({selected: false}), "Cached Shaders")) { ui.indent(); for (c in iron.data.Data.cachedShaders) { ui.text(c.name); } ui.unindent(); } // if (ui.panel(Id.handle({selected: false}), 'Cached Textures')) { // ui.indent(); // for (c in iron.data.Data.cachedImages) { // ui.image(c); // } // ui.unindent(); // } } if (ui.tab(htab, lastTraces[0] == "" ? "Console" : lastTraces[0].substr(0, 20))) { #if js if (ui.panel(Id.handle({selected: false}), "Script")) { ui.indent(); var t = ui.textInput(Id.handle()); if (ui.button("Run")) { try { trace("> " + t); js.Lib.eval(t); } catch (e: Dynamic) { trace(e); } } ui.unindent(); } #end if (ui.panel(Id.handle({selected: true}), "Log")) { ui.indent(); if (ui.button("Clear")) { lastTraces[0] = ""; lastTraces.splice(1, lastTraces.length - 1); } for (t in lastTraces) ui.text(t); ui.unindent(); } } if (watchNodes.length > 0 && ui.tab(htab, "Watch")) { for (n in watchNodes) { ui.text(n.tree.object.name + "." + n.tree.name + "." + n.name + " = " + n.get(0)); } } ui.separator(); } // Draw trait debug windows var handleWinTrait = Id.handle(); for (trait in selectedTraits) { var objectID = trait.object.uid; var traitIndex = trait.object.traits.indexOf(trait); var handleWindow = handleWinTrait.nest(objectID).nest(traitIndex); // This solution is not optimal, dragged windows will change their // position if the selectedTraits array is changed. wx -= ww + 8; wy = 0; handleWindow.redraws = 1; ui.window(handleWindow, wx, wy, ww, wh, true); if (ui.button("Close Trait View")) { selectedTraits.remove(trait); handleWinTrait.nest(objectID).unnest(traitIndex); continue; } ui.row([1/2, 1/2]); ui.text("Trait:"); ui.text(Type.getClassName(Type.getClass(trait)), Align.Right); ui.row([1/2, 1/2]); ui.text("Extends:"); ui.text(Type.getClassName(Type.getSuperClass(Type.getClass(trait))), Align.Right); ui.row([1/2, 1/2]); ui.text("Object:"); ui.text(trait.object.name, Align.Right); ui.separator(); if (ui.panel(Id.handle().nest(objectID).nest(traitIndex), "Attributes")) { ui.indent(); for (fieldName in Reflect.fields(trait)) { ui.row([1/2, 1/2]); ui.text(fieldName + ""); var fieldValue = Reflect.field(trait, fieldName); var fieldClass = Type.getClass(fieldValue); // Treat objects differently (VERY bad performance otherwise) if (Reflect.isObject(fieldValue) && fieldClass != String) { if (fieldClass != null) { ui.text('<${Type.getClassName(fieldClass)}>', Align.Right); } else { // Anonymous data structures for example ui.text("", Align.Right); } } else { ui.text(Std.string(fieldValue), Align.Right); } } ui.unindent(); } } ui.end(bindG); if (bindG) g.begin(false); totalTime += frameTime; renderPathTime += iron.App.renderPathTime; frames++; if (totalTime > 1.0) { hwin.redraws = 1; var t = totalTime / frames; // Second frame if (frameTimeAvg > 0) { if (t < frameTimeAvgMin || frameTimeAvgMin == 0) frameTimeAvgMin = t; if (t > frameTimeAvgMax || frameTimeAvgMax == 0) frameTimeAvgMax = t; } frameTimeAvg = t; if (benchmark) { benchFrames++; if (benchFrames > 10) benchTime += t; if (benchFrames == 20) trace(Std.int((benchTime / 10) * 1000000) / 1000); // ms } renderPathTimeAvg = renderPathTime / frames; updateTimeAvg = updateTime / frames; animTimeAvg = animTime / frames; physTimeAvg = physTime / frames; totalTime = 0; renderPathTime = 0; updateTime = 0; animTime = 0; physTime = 0; frames = 0; if (htab.position == 2) { g.end(); updateGraph(); // Profile tab selected g.begin(false); } } frameTime = Scheduler.realTime() - lastTime; lastTime = Scheduler.realTime(); } function update() { armory.trait.WalkNavigation.enabled = !(ui.isScrolling || ui.dragHandle != null); updateTime += iron.App.updateTime; animTime += iron.object.Animation.animationTime; #if arm_physics physTime += armory.trait.physics.PhysicsWorld.physTime; #end } static function roundfp(f: Float, precision = 2): Float { f *= Math.pow(10, precision); return Math.round(f) / Math.pow(10, precision); } #end }