logo

Babylon.js Market

By Lawrence

8 minutes

Hacking the Babylon Inspector

The Babylon Inspector is powerful out of the box, but it becomes truly indispensable when you bend it to your will. We've covered the basics of getting it running - now let's dive into the juicy stuff: extending it with custom panels, injecting your own debugging data, and creating developer tools that actually solve your specific problems.

Beyond the Basic Toggle

Sure, pressing backtick to show/hide the inspector is neat, but that's just the appetizer. But what about hooking into the inspector's lifecycle and adding our own functionality.

src/components/Inspector.ts
export class InspectorSystem extends System {
  private inspectorExtensions: Map<string, any> = new Map();

  loadEntity(entity: Entity, deltaTime: number) {
    const inspectorComponent = entity.getComponent(InspectorComponent);
    const { triggerKey, debugLayerId } = inspectorComponent;

    window.addEventListener("keydown", (ev) => {
      if (ev.code === triggerKey) {
        if (scene.debugLayer?.isVisible()) {
          scene.debugLayer.hide();
          this.cleanupExtensions();
        } else {
          scene.debugLayer
            .show({
              overlay: true,
              embedMode: true,
              globalRoot: document.getElementById(debugLayerId) as HTMLElement,
            })
            .then(() => {
              this.initializeCustomExtensions();
              this.setupCameraToggle();
            });
        }
      }
    });
  }
}

Injecting Custom Debug Data

One of the most powerful features is the ability to inject your own debug information directly into the inspector. This is perfect for displaying game state, performance metrics, or any custom data that's relevant to your project.

src/components/Inspector.ts
private initializeCustomExtensions() {
  // Add custom debug information to nodes
  this.scene.meshes.forEach(mesh => {
    if (!mesh.metadata) mesh.metadata = {};

    // Inject custom properties that show up in the inspector
    mesh.metadata.customDebugInfo = {
      entityId: this.getEntityIdForMesh(mesh),
      componentCount: this.getComponentCountForMesh(mesh),
      lastUpdated: Date.now(),
      performance: {
        renderTime: this.getLastRenderTime(mesh),
        triangleCount: mesh.getTotalVertices() / 3,
        memoryUsage: this.estimateMemoryUsage(mesh)
      }
    };
  });

  // Create custom inspector tabs
  this.addPerformanceTab();
  this.addEntitySystemTab();
  this.addSceneStatisticsTab();
}

private addPerformanceTab() {
  const inspector = this.scene.debugLayer;
  if (!inspector) return;

  const customPanel = document.createElement('div');
  customPanel.id = 'performance-panel';
  customPanel.innerHTML = `
    <div class="custom-debug-panel">
      <h3>Performance Monitor</h3>
      <div id="fps-counter">FPS: --</div>
      <div id="draw-calls">Draw Calls: --</div>
      <div id="active-meshes">Active Meshes: --</div>
      <canvas id="performance-graph" width="300" height="100"></canvas>
    </div>
  `;

  // Find the inspector's tab container and add our custom tab
  const tabContainer = document.querySelector('.inspector-header');
  if (tabContainer) {
    const customTab = document.createElement('div');
    customTab.class = 'tab custom-tab';
    customTab.textContent = 'Performance';
    customTab.onclick = () => this.showPerformancePanel();
    tabContainer.appendChild(customTab);
  }
}

Real-Time Performance Monitoring

Let's create a performance monitor that updates in real-time and displays meaningful data about your scene:

src/components/Inspector.ts
private setupPerformanceMonitoring() {
  const performanceData: number[] = [];
  const maxDataPoints = 60; // Keep last 60 frames

  this.scene.registerBeforeRender(() => {
    if (!this.scene.debugLayer?.isVisible()) return;

    const engine = this.scene.getEngine();
    const fps = engine.getFps();
    const drawCalls = engine.drawCalls;
    const activeMeshes = this.scene.getActiveMeshes().length;

    // Update FPS graph
    performanceData.push(fps);
    if (performanceData.length > maxDataPoints) {
      performanceData.shift();
    }

    // Update UI elements
    const fpsElement = document.getElementById('fps-counter');
    const drawCallsElement = document.getElementById('draw-calls');
    const activeMeshesElement = document.getElementById('active-meshes');

    if (fpsElement) fpsElement.textContent = `FPS: ${fps.toFixed(1)}`;
    if (drawCallsElement) drawCallsElement.textContent = `Draw Calls: ${drawCalls}`;
    if (activeMeshesElement) activeMeshesElement.textContent = `Active Meshes: ${activeMeshes}`;

    // Draw performance graph
    this.drawPerformanceGraph(performanceData);
  });
}

private drawPerformanceGraph(data: number[]) {
  const canvas = document.getElementById('performance-graph') as HTMLCanvasElement;
  if (!canvas) return;

  const ctx = canvas.getContext('2d');
  if (!ctx) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw grid
  ctx.strokeStyle = '#333';
  ctx.lineWidth = 0.5;
  for (let i = 0; i < 5; i++) {
    const y = (canvas.height / 4) * i;
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(canvas.width, y);
    ctx.stroke();
  }

  // Draw FPS line
  if (data.length > 1) {
    ctx.strokeStyle = '#00ff00';
    ctx.lineWidth = 2;
    ctx.beginPath();

    const maxFps = Math.max(...data, 60);
    data.forEach((fps, index) => {
      const x = (canvas.width / data.length) * index;
      const y = canvas.height - ((fps / maxFps) * canvas.height);

      if (index === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    });

    ctx.stroke();
  }
}

Entity Component System Integration

If you're using an ECS architecture (like in our example), you can create custom inspector panels that show entity and component information:

src/components/Inspector.ts
private addEntitySystemTab() {
  const customPanel = document.createElement('div');
  customPanel.id = 'ecs-panel';
  customPanel.innerHTML = `
    <div class="custom-debug-panel">
      <h3>Entity Component System</h3>
      <div class="ecs-controls">
        <button id="refresh-entities">Refresh</button>
        <input type="text" id="entity-filter" placeholder="Filter entities...">
      </div>
      <div id="entity-list"></div>
      <div id="component-details"></div>
    </div>
  `;

  // Add event listeners
  setTimeout(() => {
    const refreshButton = document.getElementById('refresh-entities');
    const filterInput = document.getElementById('entity-filter') as HTMLInputElement;

    if (refreshButton) {
      refreshButton.onclick = () => this.refreshEntityList();
    }

    if (filterInput) {
      filterInput.oninput = (e) => {
        const target = e.target as HTMLInputElement;
        this.filterEntities(target.value);
      };
    }

    this.refreshEntityList();
  }, 100);
}

private refreshEntityList() {
  const entityListElement = document.getElementById('entity-list');
  if (!entityListElement) return;

  const entities = this.world.entities;
  entityListElement.innerHTML = '';

  entities.forEach((entity, id) => {
    const entityElement = document.createElement('div');
    entityElement.class = 'entity-item';
    entityElement.innerHTML = `
      <div class="entity-header" onclick="toggleEntity('${id}')">
        <span class="entity-id">Entity ${id}</span>
        <span class="component-count">${entity.components.size} components</span>
      </div>
      <div class="entity-components" id="components-${id}" style="display: none;">
        ${Array.from(entity.components.values()).map(component =>
          `<div class="component-item">${component.constructor.name}</div>`
        ).join('')}
      </div>
    `;

    entityListElement.appendChild(entityElement);
  });
}

Advanced Camera Controls

Remember that basic camera toggle we had? Let's make it actually useful by creating a proper debug camera system:

src/components/Inspector.ts
private setupAdvancedCameraControls() {

Continue Reading

Unlock the full course to access all content and examples.

$8
↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search