Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Babylon.js 8 Advanced 3D Application</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.babylonjs.com/babylon.js"></script> | |
| <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script> | |
| <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script> | |
| <script src="https://cdn.babylonjs.com/proceduralTexturesLibrary/babylon.proceduralTextures.min.js"></script> | |
| <script src="https://cdn.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script> | |
| <style> | |
| #renderCanvas { | |
| width: 100%; | |
| height: 100%; | |
| touch-action: none; | |
| outline: none; | |
| } | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.9); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| transition: opacity 0.5s; | |
| } | |
| .progress-bar { | |
| width: 300px; | |
| height: 4px; | |
| background: #444; | |
| margin-top: 20px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #3b82f6, #8b5cf6); | |
| width: 0%; | |
| transition: width 0.3s; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| .tooltip { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .tooltip .tooltip-text { | |
| visibility: hidden; | |
| width: 200px; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 6px; | |
| padding: 8px; | |
| position: absolute; | |
| z-index: 1; | |
| bottom: 125%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .tooltip:hover .tooltip-text { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| .panel-slide { | |
| transition: transform 0.3s ease, opacity 0.3s ease; | |
| } | |
| .panel-slide.hidden { | |
| transform: translateY(100%); | |
| opacity: 0; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100 overflow-hidden"> | |
| <!-- Loading Screen --> | |
| <div id="loadingOverlay" class="loading-overlay"> | |
| <div class="text-4xl font-bold mb-4 bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent"> | |
| Loading 3D World | |
| </div> | |
| <div class="text-gray-400 mb-2">Initializing Babylon.js 8...</div> | |
| <div class="progress-bar"> | |
| <div id="progressFill" class="progress-fill"></div> | |
| </div> | |
| </div> | |
| <!-- Main Container --> | |
| <div class="flex flex-col h-screen"> | |
| <!-- Top Status Bar --> | |
| <div class="bg-gray-800 py-2 px-4 flex justify-between items-center"> | |
| <div class="text-xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent"> | |
| Babylon.js 8 Advanced Demo | |
| </div> | |
| <div class="flex space-x-2"> | |
| <div id="fpsCounter" class="px-3 py-1 bg-gray-700 rounded-full text-xs">0 FPS</div> | |
| <div id="objectCount" class="px-3 py-1 bg-gray-700 rounded-full text-xs">0 Objects</div> | |
| <div id="sceneInfo" class="px-3 py-1 bg-gray-700 rounded-full text-xs">Default Scene</div> | |
| </div> | |
| </div> | |
| <!-- Main Content Area --> | |
| <div class="flex flex-1 overflow-hidden"> | |
| <!-- Side Panel (Collapsible) --> | |
| <div id="sidePanel" class="w-64 bg-gray-800 border-r border-gray-700 flex flex-col transition-all duration-300 ease-in-out" style="min-width: 16rem;"> | |
| <div class="p-4 border-b border-gray-700 flex justify-between items-center"> | |
| <h2 class="text-lg font-semibold">Scene Controls</h2> | |
| <button id="togglePanel" class="text-gray-400 hover:text-white"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M15.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="flex-1 overflow-y-auto"> | |
| <!-- Scene Management --> | |
| <div class="p-4 border-b border-gray-700"> | |
| <h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Scene Management</h3> | |
| <div class="space-y-2"> | |
| <button id="newScene" class="w-full px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm flex items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> | |
| </svg> | |
| New Scene | |
| </button> | |
| <button id="saveScene" class="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm flex items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" /> | |
| </svg> | |
| Save Scene | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Environment Settings --> | |
| <div class="p-4 border-b border-gray-700"> | |
| <h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Environment</h3> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="block text-xs text-gray-400 mb-1">Skybox</label> | |
| <select id="skyboxSelect" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <option value="default">Default</option> | |
| <option value="sunset">Sunset</option> | |
| <option value="night">Night</option> | |
| <option value="studio">Studio</option> | |
| <option value="park">Park</option> | |
| <option value="none">None</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-xs text-gray-400 mb-1">Environment Intensity</label> | |
| <input id="envIntensity" type="range" min="0" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <label class="text-xs text-gray-400">Fog</label> | |
| <button id="toggleFog" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs"> | |
| Toggle | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Lighting --> | |
| <div class="p-4 border-b border-gray-700"> | |
| <h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Lighting</h3> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="block text-xs text-gray-400 mb-1">Hemispheric Light</label> | |
| <div class="flex space-x-2"> | |
| <button id="toggleHemiLight" class="flex-1 bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs"> | |
| Toggle | |
| </button> | |
| <button id="adjustHemiLight" class="flex-1 bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs"> | |
| Adjust | |
| </button> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-xs text-gray-400 mb-1">Directional Light</label> | |
| <div class="flex space-x-2"> | |
| <button id="toggleDirLight" class="flex-1 bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs"> | |
| Toggle | |
| </button> | |
| <button id="adjustDirLight" class="flex-1 bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs"> | |
| Adjust | |
| </button> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-xs text-gray-400 mb-1">Light Intensity</label> | |
| <input id="lightIntensity" type="range" min="0" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Objects --> | |
| <div class="p-4 border-b border-gray-700"> | |
| <h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Objects</h3> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button id="addSphere" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-2 text-xs flex flex-col items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /> | |
| </svg> | |
| Sphere | |
| </button> | |
| <button id="addBox" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-2 text-xs flex flex-col items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /> | |
| </svg> | |
| Box | |
| </button> | |
| <button id="addCylinder" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-2 text-xs flex flex-col items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18.5V20m0 0v1.5m0-1.5h-1.5m1.5 0h1.5M5 10v4h14v-4H5z" /> | |
| </svg> | |
| Cylinder | |
| </button> | |
| <button id="addPlane" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-2 text-xs flex flex-col items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> | |
| </svg> | |
| Plane | |
| </button> | |
| </div> | |
| <div class="mt-3"> | |
| <button id="clearObjects" class="w-full bg-red-600 hover:bg-red-700 rounded px-3 py-2 text-sm flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
| </svg> | |
| Clear All | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Materials --> | |
| <div class="p-4 border-b border-gray-700"> | |
| <h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Materials</h3> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="block text-xs text-gray-400 mb-1">Material Type</label> | |
| <select id="materialType" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <option value="standard">Standard</option> | |
| <option value="pbr">PBR</option> | |
| <option value="gradient">Gradient</option> | |
| <option value="grid">Grid</option> | |
| <option value="brick">Brick</option> | |
| </select> | |
| </div> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button id="colorRed" class="bg-red-600 hover:bg-red-700 h-6 rounded"></button> | |
| <button id="colorGreen" class="bg-green-600 hover:bg-green-700 h-6 rounded"></button> | |
| <button id="colorBlue" class="bg-blue-600 hover:bg-blue-700 h-6 rounded"></button> | |
| <button id="colorYellow" class="bg-yellow-600 hover:bg-yellow-700 h-6 rounded"></button> | |
| <button id="colorPurple" class="bg-purple-600 hover:bg-purple-700 h-6 rounded"></button> | |
| <button id="colorGray" class="bg-gray-600 hover:bg-gray-700 h-6 rounded"></button> | |
| </div> | |
| <div> | |
| <label class="block text-xs text-gray-400 mb-1">Texture Scale</label> | |
| <input id="textureScale" type="range" min="0.1" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Post Processing --> | |
| <div class="p-4"> | |
| <h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Effects</h3> | |
| <div class="space-y-2"> | |
| <div class="flex items-center justify-between"> | |
| <label class="text-xs">Bloom</label> | |
| <button id="toggleBloom" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs"> | |
| Enable | |
| </button> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <label class="text-xs">Depth of Field</label> | |
| <button id="toggleDOF" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs"> | |
| Disable | |
| </button> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <label class="text-xs">Grain</label> | |
| <button id="toggleGrain" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs"> | |
| Enable | |
| </button> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <label class="text-xs">SSAO</label> | |
| <button id="toggleSSAO" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs"> | |
| Enable | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 3D Viewport --> | |
| <div class="flex-1 relative"> | |
| <canvas id="renderCanvas"></canvas> | |
| <!-- Viewport Controls --> | |
| <div class="absolute bottom-4 right-4 flex space-x-2"> | |
| <button id="resetCamera" class="p-2 bg-gray-800 hover:bg-gray-700 rounded-full shadow-lg tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" /> | |
| </svg> | |
| <span class="tooltip-text">Reset Camera</span> | |
| </button> | |
| <button id="screenshot" class="p-2 bg-gray-800 hover:bg-gray-700 rounded-full shadow-lg tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| </svg> | |
| <span class="tooltip-text">Take Screenshot</span> | |
| </button> | |
| <button id="toggleFullscreen" class="p-2 bg-gray-800 hover:bg-gray-700 rounded-full shadow-lg tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" /> | |
| </svg> | |
| <span class="tooltip-text">Toggle Fullscreen</span> | |
| </button> | |
| </div> | |
| <!-- Mini Stats --> | |
| <div class="absolute top-4 left-4 bg-black bg-opacity-70 rounded-lg p-2 text-xs"> | |
| <div>Objects: <span id="miniObjCount">0</span></div> | |
| <div>FPS: <span id="miniFps">0</span></div> | |
| <div>Draw Calls: <span id="miniDrawCalls">0</span></div> | |
| </div> | |
| <!-- Selection Info --> | |
| <div id="selectionInfo" class="absolute top-4 right-4 bg-black bg-opacity-70 rounded-lg p-3 max-w-xs hidden"> | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="font-semibold">Selected Object</h3> | |
| <button id="closeSelection" class="text-gray-400 hover:text-white"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="text-sm"> | |
| <div id="selectedName" class="truncate mb-1">Name:</div> | |
| <div id="selectedType" class="text-gray-400 text-xs mb-2">Type:</div> | |
| <div class="grid grid-cols-2 gap-2 text-xs"> | |
| <div>Position: <span id="selectedPosition">0,0,0</span></div> | |
| <div>Rotation: <span id="selectedRotation">0,0,0</span></div> | |
| <div>Scaling: <span id="selectedScaling">1,1,1</span></div> | |
| <div>Material: <span id="selectedMaterial">Standard</span></div> | |
| </div> | |
| </div> | |
| <div class="mt-3 pt-2 border-t border-gray-700"> | |
| <button id="deleteSelected" class="w-full bg-red-600 hover:bg-red-700 rounded px-2 py-1 text-xs"> | |
| Delete | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- WebGPU Warning Modal --> | |
| <div id="webgpuModal" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-gray-800 rounded-lg p-6 max-w-md mx-4"> | |
| <div class="text-xl font-bold mb-4 text-yellow-400 flex items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> | |
| </svg> | |
| WebGPU Not Available | |
| </div> | |
| <p class="mb-4 text-gray-300"> | |
| Your browser doesn't support WebGPU or it's not enabled. The application will fall back to WebGL, which may affect performance. | |
| </p> | |
| <div class="flex justify-end space-x-3"> | |
| <button id="continueButton" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded"> | |
| Continue Anyway | |
| </button> | |
| <button id="learnButton" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded"> | |
| Learn More | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Screenshot Preview --> | |
| <div id="screenshotPreview" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-gray-900 rounded-lg p-2 shadow-2xl max-w-5xl"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <h3 class="text-lg font-semibold">Screenshot Preview</h3> | |
| <button id="closePreview" class="text-gray-400 hover:text-white p-1"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <img id="previewImage" class="max-w-full max-h-[80vh]" src="" alt="Screenshot preview"> | |
| <div class="flex justify-between mt-3"> | |
| <button id="downloadScreenshot" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded"> | |
| Download | |
| </button> | |
| <button id="copyScreenshot" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded"> | |
| Copy to Clipboard | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Immediately Invoked Function Expression (IIFE) to encapsulate the application | |
| const BabylonApp = (() => { | |
| // Private variables | |
| let engine, scene, canvas; | |
| let hemisphericLight, directionalLight, shadowGenerator; | |
| let advancedTexture, fpsLabel, objectCountLabel; | |
| let bloomEffect, depthOfFieldEffect, grainEffect, ssaoEffect; | |
| let currentSelection = null; | |
| // Configuration | |
| const config = { | |
| defaultScene: { | |
| groundSize: 20, | |
| groundSegments: 64, | |
| skyboxPath: 'https://assets.babylonjs.com/environments/skybox', | |
| skyboxFiles: ['px.png', 'py.png', 'pz.png', 'nx.png', 'ny.png', 'nz.png'] | |
| }, | |
| materials: { | |
| colors: { | |
| red: new BABYLON.Color3(0.8, 0.2, 0.2), | |
| green: new BABYLON.Color3(0.2, 0.8, 0.2), | |
| blue: new BABYLON.Color3(0.2, 0.2, 0.8), | |
| yellow: new BABYLON.Color3(0.8, 0.8, 0.2), | |
| purple: new BABYLON.Color3(0.6, 0.2, 0.8), | |
| gray: new BABYLON.Color3(0.5, 0.5, 0.5) | |
| } | |
| }, | |
| performance: { | |
| updateInterval: 500 // ms | |
| } | |
| }; | |
| // State management | |
| const state = { | |
| currentMaterial: 'standard', | |
| currentColor: config.materials.colors.red, | |
| postProcessing: { | |
| bloom: false, | |
| depthOfField: false, | |
| grain: false, | |
| ssao: false | |
| }, | |
| lights: { | |
| hemispheric: true, | |
| directional: true, | |
| intensity: 1 | |
| }, | |
| environment: { | |
| skybox: 'default', | |
| intensity: 1, | |
| fog: false | |
| } | |
| }; | |
| // DOM elements | |
| const dom = { | |
| // Loading screen | |
| loadingOverlay: document.getElementById('loadingOverlay'), | |
| progressFill: document.getElementById('progressFill'), | |
| // Panels | |
| sidePanel: document.getElementById('sidePanel'), | |
| togglePanel: document.getElementById('togglePanel'), | |
| // Status bars | |
| fpsCounter: document.getElementById('fpsCounter'), | |
| objectCount: document.getElementById('objectCount'), | |
| sceneInfo: document.getElementById('sceneInfo'), | |
| miniFps: document.getElementById('miniFps'), | |
| miniObjCount: document.getElementById('miniObjCount'), | |
| miniDrawCalls: document.getElementById('miniDrawCalls'), | |
| // Selection info | |
| selectionInfo: document.getElementById('selectionInfo'), | |
| closeSelection: document.getElementById('closeSelection'), | |
| selectedName: document.getElementById('selectedName'), | |
| selectedType: document.getElementById('selectedType'), | |
| selectedPosition: document.getElementById('selectedPosition'), | |
| selectedRotation: document.getElementById('selectedRotation'), | |
| selectedScaling: document.getElementById('selectedScaling'), | |
| selectedMaterial: document.getElementById('selectedMaterial'), | |
| deleteSelected: document.getElementById('deleteSelected'), | |
| // Modals | |
| webgpuModal: document.getElementById('webgpuModal'), | |
| continueButton: document.getElementById('continueButton'), | |
| learnButton: document.getElementById('learnButton'), | |
| screenshotPreview: document.getElementById('screenshotPreview'), | |
| closePreview: document.getElementById('closePreview'), | |
| previewImage: document.getElementById('previewImage'), | |
| downloadScreenshot: document.getElementById('downloadScreenshot'), | |
| copyScreenshot: document.getElementById('copyScreenshot'), | |
| // Scene management | |
| newScene: document.getElementById('newScene'), | |
| saveScene: document.getElementById('saveScene'), | |
| // Environment | |
| skyboxSelect: document.getElementById('skyboxSelect'), | |
| envIntensity: document.getElementById('envIntensity'), | |
| toggleFog: document.getElementById('toggleFog'), | |
| // Lights | |
| toggleHemiLight: document.getElementById('toggleHemiLight'), | |
| toggleDirLight: document.getElementById('toggleDirLight'), | |
| adjustHemiLight: document.getElementById('adjustHemiLight'), | |
| adjustDirLight: document.getElementById('adjustDirLight'), | |
| lightIntensity: document.getElementById('lightIntensity'), | |
| // Objects | |
| addSphere: document.getElementById('addSphere'), | |
| addBox: document.getElementById('addBox'), | |
| addCylinder: document.getElementById('addCylinder'), | |
| addPlane: document.getElementById('addPlane'), | |
| clearObjects: document.getElementById('clearObjects'), | |
| // Materials | |
| materialType: document.getElementById('materialType'), | |
| colorRed: document.getElementById('colorRed'), | |
| colorGreen: document.getElementById('colorGreen'), | |
| colorBlue: document.getElementById('colorBlue'), | |
| colorYellow: document.getElementById('colorYellow'), | |
| colorPurple: document.getElementById('colorPurple'), | |
| colorGray: document.getElementById('colorGray'), | |
| textureScale: document.getElementById('textureScale'), | |
| // Effects | |
| toggleBloom: document.getElementById('toggleBloom'), | |
| toggleDOF: document.getElementById('toggleDOF'), | |
| toggleGrain: document.getElementById('toggleGrain'), | |
| toggleSSAO: document.getElementById('toggleSSAO'), | |
| // Viewport controls | |
| resetCamera: document.getElementById('resetCamera'), | |
| screenshot: document.getElementById('screenshot'), | |
| toggleFullscreen: document.getElementById('toggleFullscreen') | |
| }; | |
| // ============================================== | |
| // Utility Functions | |
| // ============================================== | |
| // Create a loading progress updater using currying | |
| const createProgressUpdater = (totalSteps) => { | |
| let currentStep = 0; | |
| return (increment = 1) => { | |
| currentStep += increment; | |
| const percentage = (currentStep / totalSteps) * 100; | |
| dom.progressFill.style.width = `${percentage}%`; | |
| // Hide loading screen when complete | |
| if (currentStep >= totalSteps) { | |
| setTimeout(() => { | |
| dom.loadingOverlay.style.opacity = '0'; | |
| setTimeout(() => { | |
| dom.loadingOverlay.style.display = 'none'; | |
| }, 500); | |
| }, 300); | |
| } | |
| }; | |
| }; | |
| // Get a random position in a donut shape around origin | |
| const getRandomPosition = (minRadius = 3, maxRadius = 8, yRange = [0.5, 3]) => { | |
| const angle = Math.random() * Math.PI * 2; | |
| const radius = minRadius + Math.random() * (maxRadius - minRadius); | |
| const x = Math.cos(angle) * radius; | |
| const z = Math.sin(angle) * radius; | |
| const y = yRange[0] + Math.random() * (yRange[1] - yRange[0]); | |
| return new BABYLON.Vector3(x, y, z); | |
| }; | |
| // Create a gradient material using higher-order functions | |
| const createMaterialFactory = (scene) => (type) => (options = {}) => { | |
| let material; | |
| switch(type) { | |
| case 'standard': | |
| material = new BABYLON.StandardMaterial(`mat_${type}_${Date.now()}`, scene); | |
| if (options.diffuseColor) material.diffuseColor = options.diffuseColor; | |
| if (options.specularColor) material.specularColor = options.specularColor; | |
| if (options.emissiveColor) material.emissiveColor = options.emissiveColor; | |
| material.alpha = options.alpha || 1; | |
| break; | |
| case 'pbr': | |
| material = new BABYLON.PBRMaterial(`mat_${type}_${Date.now()}`, scene); | |
| if (options.albedoColor) material.albedoColor = options.albedoColor; | |
| if (options.metallic !== undefined) material.metallic = options.metallic; | |
| if (options.roughness !== undefined) material.roughness = options.roughness; | |
| material.alpha = options.alpha || 1; | |
| break; | |
| case 'gradient': | |
| material = new BABYLON.GradientMaterial(`mat_${type}_${Date.now()}`, scene); | |
| if (options.topColor) material.topColor = options.topColor; | |
| if (options.bottomColor) material.bottomColor = options.bottomColor; | |
| if (options.offset !== undefined) material.offset = options.offset; | |
| if (options.scale !== undefined) material.scale = options.scale; | |
| break; | |
| case 'grid': | |
| material = new BABYLON.GridMaterial(`mat_${type}_${Date.now()}`, scene); | |
| if (options.mainColor) material.mainColor = options.mainColor; | |
| if (options.lineColor) material.lineColor = options.lineColor; | |
| if (options.gridRatio !== undefined) material.gridRatio = options.gridRatio; | |
| if (options.scale !== undefined) material.scale = options.scale; | |
| break; | |
| case 'brick': | |
| const brickTexture = new BABYLON.BrickProceduralTexture('brickTex', 512, scene); | |
| if (options.brickColor) brickTexture.brickColor = options.brickColor; | |
| if (options.jointColor) brickTexture.jointColor = options.jointColor; | |
| if (options.numberOfBricksHeight !== undefined) brickTexture.numberOfBricksHeight = options.numberOfBricksHeight; | |
| material = new BABYLON.StandardMaterial(`mat_${type}_${Date.now()}`, scene); | |
| material.diffuseTexture = brickTexture; | |
| break; | |
| } | |
| return material; | |
| }; | |
| // Format position/rotation/scaling for display | |
| const formatVector = (vector) => { | |
| const x = vector.x.toFixed(2); | |
| const y = vector.y.toFixed(2); | |
| const z = vector.z.toFixed(2); | |
| return `${x}, ${y}, ${z}`; | |
| }; | |
| // ============================================== | |
| // Scene Initialization | |
| // ============================================== | |
| // Initialize the Babylon.js engine and scene | |
| const initScene = async () => { | |
| const updateProgress = createProgressUpdater(10); | |
| try { | |
| // Check for WebGPU support | |
| const webGPUSupported = await BABYLON.WebGPUEngine.IsSupportedAsync; | |
| if (!webGPUSupported) { | |
| dom.webgpuModal.classList.remove('hidden'); | |
| updateProgress(1); | |
| } | |
| // Get the canvas | |
| canvas = document.getElementById('renderCanvas'); | |
| // Initialize the engine | |
| engine = new BABYLON.Engine( | |
| canvas, | |
| true, | |
| { | |
| preserveDrawingBuffer: true, | |
| stencil: true, | |
| antialias: true, | |
| adaptToDeviceRatio: true | |
| } | |
| ); | |
| updateProgress(1); | |
| // Create the scene | |
| scene = new BABYLON.Scene(engine); | |
| scene.autoClear = false; | |
| scene.autoClearDepthAndStencil = false; | |
| updateProgress(1); | |
| // Create camera | |
| const camera = new BABYLON.ArcRotateCamera( | |
| "camera", | |
| -Math.PI / 2, | |
| Math.PI / 2, | |
| 15, | |
| BABYLON.Vector3.Zero(), | |
| scene | |
| ); | |
| camera.attachControl(canvas, true); | |
| camera.lowerRadiusLimit = 5; | |
| camera.upperRadiusLimit = 30; | |
| camera.wheelDeltaPercentage = 0.01; | |
| camera.panningSensibility = 50; | |
| camera.allowUpsideDown = false; | |
| updateProgress(1); | |
| // Create default lights | |
| setupLights(); | |
| updateProgress(1); | |
| // Create ground | |
| createGround(); | |
| updateProgress(1); | |
| // Setup shadow generator | |
| setupShadows(); | |
| updateProgress(1); | |
| // Setup skybox | |
| setupSkybox('default'); | |
| updateProgress(1); | |
| // Setup post-processing | |
| setupPostProcessing(); | |
| updateProgress(1); | |
| // Setup GUI | |
| setupGUI(); | |
| updateProgress(1); | |
| // Setup event listeners | |
| setupEventListeners(); | |
| updateProgress(1); | |
| // Setup picking/selection | |
| setupPicking(); | |
| updateProgress(1); | |
| // Run the render loop | |
| engine.runRenderLoop(() => { | |
| scene.render(); | |
| updateStats(); | |
| }); | |
| // Handle resize | |
| window.addEventListener('resize', () => { | |
| engine.resize(); | |
| }); | |
| } catch (error) { | |
| console.error("Error initializing scene:", error); | |
| } | |
| }; | |
| // ============================================== | |
| // Scene Setup Functions | |
| // ============================================== | |
| // Setup lights in the scene | |
| const setupLights = () => { | |
| // Hemispheric light (ambient) | |
| hemisphericLight = new BABYLON.HemisphericLight( | |
| "hemiLight", | |
| new BABYLON.Vector3(0, 1, 0), | |
| scene | |
| ); | |
| hemisphericLight.intensity = state.lights.intensity; | |
| hemisphericLight.diffuse = new BABYLON.Color3(1, 1, 1); | |
| hemisphericLight.groundColor = new BABYLON.Color3(0.5, 0.5, 0.5); | |
| // Directional light (sun-like) | |
| directionalLight = new BABYLON.DirectionalLight( | |
| "dirLight", | |
| new BABYLON.Vector3(-1, -2, -1), | |
| scene | |
| ); | |
| directionalLight.position = new BABYLON.Vector3(0, 10, 0); | |
| directionalLight.intensity = state.lights.intensity * 0.8; | |
| directionalLight.shadowMinZ = 1; | |
| directionalLight.shadowMaxZ = 20; | |
| }; | |
| // Setup ground in the scene | |
| const createGround = () => { | |
| const ground = BABYLON.MeshBuilder.CreateGround( | |
| "ground", | |
| { | |
| width: config.defaultScene.groundSize, | |
| height: config.defaultScene.groundSize, | |
| subdivisions: config.defaultScene.groundSegments | |
| }, | |
| scene | |
| ); | |
| const material = createMaterialFactory(scene)('pbr')({ | |
| albedoColor: new BABYLON.Color3(0.2, 0.2, 0.25), | |
| metallic: 0.1, | |
| roughness: 0.8 | |
| }); | |
| ground.material = material; | |
| ground.receiveShadows = true; | |
| shadowGenerator.addShadowCaster(ground); | |
| }; | |
| // Setup shadows in the scene | |
| const setupShadows = () => { | |
| shadowGenerator = new BABYLON.ShadowGenerator(2048, directionalLight); | |
| shadowGenerator.useBlurExponentialShadowMap = true; | |
| shadowGenerator.blurKernel = 32; | |
| shadowGenerator.bias = 0.0001; | |
| shadowGenerator.normalBias = 0.02; | |
| shadowGenerator.forceBackFacesOnly = false; | |
| }; | |
| // Setup skybox | |
| const setupSkybox = (type) => { | |
| // Remove existing skybox if any | |
| if (scene.skybox) { | |
| scene.skybox.dispose(); | |
| } | |
| if (type === 'none') return; | |
| let skyboxPath = config.defaultScene.skyboxPath; | |
| if (type !== 'default') { | |
| skyboxPath = `https://assets.babylonjs.com/environments/${type}env`; | |
| } | |
| scene.environmentTexture = BABYLON.CubeTexture.CreateFromPrefilteredData( | |
| skyboxPath + ".env", | |
| scene | |
| ); | |
| if (type !== 'default') { | |
| const skybox = scene.createDefaultSkybox(scene.environmentTexture, true, 1000); | |
| skybox.ignorePicking = true; | |
| } | |
| }; | |
| // Setup post-processing effects | |
| const setupPostProcessing = () => { | |
| // Create default pipeline | |
| const defaultPipeline = new BABYLON.DefaultRenderingPipeline( | |
| "defaultPipeline", | |
| true, | |
| scene, | |
| [scene.activeCamera] | |
| ); | |
| // Bloom effect | |
| bloomEffect = new BABYLON.BloomEffect( | |
| "bloom", | |
| scene, | |
| 1.0, // Bloom scale | |
| 0.8, // Bloom threshold | |
| 4.0, // Bloom weight | |
| 200 // Blur kernel size | |
| ); | |
| bloomEffect.enabled = state.postProcessing.bloom; | |
| defaultPipeline.addEffect(bloomEffect); | |
| // Depth of Field | |
| depthOfFieldEffect = new BABYLON.DepthOfFieldEffect( | |
| scene, | |
| { target: scene.activeCamera, focusDistance: 0.1, fStop: 1.4 } | |
| ); | |
| depthOfFieldEffect.enabled = state.postProcessing.depthOfField; | |
| defaultPipeline.addEffect(depthOfFieldEffect); | |
| // Grain effect | |
| grainEffect = new BABYLON.GrainEffect("grain", scene); | |
| grainEffect.enabled = state.postProcessing.grain; | |
| grainEffect.intensity = 0.3; | |
| defaultPipeline.addEffect(grainEffect); | |
| // SSAO effect | |
| ssaoEffect = new BABYLON.SSAORenderingPipeline( | |
| "ssao", | |
| scene, | |
| { ssaoRatio: 0.5, combineRatio: 0.5 } | |
| ); | |
| ssaoEffect.enabled = state.postProcessing.ssao; | |
| }; | |
| // Setup GUI | |
| const setupGUI = () => { | |
| // Advanced Texture for GUI | |
| advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI"); | |
| // FPS Label | |
| fpsLabel = new BABYLON.GUI.TextBlock(); | |
| fpsLabel.text = "FPS: 0"; | |
| fpsLabel.color = "white"; | |
| fpsLabel.fontSize = 14; | |
| fpsLabel.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; | |
| fpsLabel.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP; | |
| fpsLabel.left = "20px"; | |
| fpsLabel.top = "20px"; | |
| fpsLabel.fontFamily = "Arial"; | |
| advancedTexture.addControl(fpsLabel); | |
| // Object Count Label | |
| objectCountLabel = new BABYLON.GUI.TextBlock(); | |
| objectCountLabel.text = "Objects: 0"; | |
| objectCountLabel.color = "white"; | |
| objectCountLabel.fontSize = 14; | |
| objectCountLabel.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; | |
| objectCountLabel.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP; | |
| objectCountLabel.left = "20px"; | |
| objectCountLabel.top = "40px"; | |
| objectCountLabel.fontFamily = "Arial"; | |
| advancedTexture.addControl(objectCountLabel); | |
| }; | |
| // Setup object picking/selection | |
| const setupPicking = () => { | |
| scene.onPointerDown = (evt, pickResult) => { | |
| if (!pickResult.hit || !pickResult.pickedMesh) { | |
| hideSelectionInfo(); | |
| currentSelection = null; | |
| return; | |
| } | |
| const mesh = pickResult.pickedMesh; | |
| // Don't select ground, skybox, etc. | |
| if (!mesh.name || mesh.name === 'ground' || mesh.name.startsWith('skybox') || mesh.name.startsWith('_default_')) { | |
| hideSelectionInfo(); | |
| currentSelection = null; | |
| return; | |
| } | |
| showSelectionInfo(mesh); | |
| currentSelection = mesh; | |
| }; | |
| }; | |
| // ============================================== | |
| // Scene Management Functions | |
| // ============================================== | |
| // Create a new primitive object in the scene | |
| const createPrimitive = (type, position = null) => { | |
| if (!scene) return; | |
| const meshPosition = position || getRandomPosition(); | |
| let mesh; | |
| switch(type) { | |
| case 'sphere': | |
| mesh = BABYLON.MeshBuilder.CreateSphere( | |
| `sphere_${Date.now()}`, | |
| { diameter: 1 + Math.random() * 2 }, | |
| scene | |
| ); | |
| break; | |
| case 'box': | |
| mesh = BABYLON.MeshBuilder.CreateBox( | |
| `box_${Date.now()}`, | |
| { | |
| size: 1 + Math.random() * 2, | |
| width: 1 + Math.random() * 2, | |
| height: 1 + Math.random() * 2, | |
| depth: 1 + Math.random() * 2 | |
| }, | |
| scene | |
| ); | |
| break; | |
| case 'cylinder': | |
| mesh = BABYLON.MeshBuilder.CreateCylinder( | |
| `cylinder_${Date.now()}`, | |
| { | |
| diameterTop: 1 + Math.random(), | |
| diameterBottom: 1 + Math.random(), | |
| height: 1 + Math.random() * 2, | |
| tessellation: 32 | |
| }, | |
| scene | |
| ); | |
| break; | |
| case 'plane': | |
| mesh = BABYLON.MeshBuilder.CreatePlane( | |
| `plane_${Date.now()}`, | |
| { | |
| size: 1 + Math.random() * 3, | |
| width: 1 + Math.random() * 3, | |
| height: 1 + Math.random() * 3 | |
| }, | |
| scene | |
| ); | |
| break; | |
| default: | |
| return; | |
| } | |
| // Position the mesh | |
| mesh.position = meshPosition; | |
| // Random rotation | |
| mesh.rotation = new BABYLON.Vector3( | |
| Math.random() * Math.PI * 2, | |
| Math.random() * Math.PI * 2, | |
| Math.random() * Math.PI * 2 | |
| ); | |
| // Random scaling | |
| const scale = 0.5 + Math.random(); | |
| mesh.scaling = new BABYLON.Vector3(scale, scale, scale); | |
| // Apply material | |
| applyCurrentMaterialToMesh(mesh); | |
| // Add to shadow generator | |
| shadowGenerator.addShadowCaster(mesh); | |
| return mesh; | |
| }; | |
| // Apply the current selected material to a mesh | |
| const applyCurrentMaterialToMesh = (mesh) => { | |
| if (!mesh) return; | |
| const materialFactory = createMaterialFactory(scene); | |
| let material; | |
| switch(state.currentMaterial) { | |
| case 'standard': | |
| material = materialFactory('standard')({ | |
| diffuseColor: state.currentColor, | |
| specularColor: new BABYLON.Color3(0.5, 0.5, 0.5), | |
| emissiveColor: new BABYLON.Color3(0.1, 0.1, 0.1) | |
| }); | |
| break; | |
| case 'pbr': | |
| material = materialFactory('pbr')({ | |
| albedoColor: state.currentColor, | |
| metallic: Math.random(), | |
| roughness: Math.random() | |
| }); | |
| break; | |
| case 'gradient': | |
| material = materialFactory('gradient')({ | |
| topColor: state.currentColor, | |
| bottomColor: state.currentColor.scale(0.5), | |
| offset: Math.random() * 2, | |
| scale: parseFloat(dom.textureScale.value) | |
| }); | |
| break; | |
| case 'grid': | |
| material = materialFactory('grid')({ | |
| mainColor: state.currentColor, | |
| lineColor: state.currentColor.scale(0.5), | |
| gridRatio: 0.2 + Math.random() * 0.3, | |
| scale: parseFloat(dom.textureScale.value) | |
| }); | |
| break; | |
| case 'brick': | |
| const brickOptions = { | |
| brickColor: state.currentColor, | |
| jointColor: state.currentColor.scale(0.3), | |
| numberOfBricksHeight: 5 + Math.floor(Math.random() * 10) | |
| }; | |
| material = materialFactory('brick')(brickOptions); | |
| break; | |
| } | |
| mesh.material = material; | |
| }; | |
| // Clear all objects from the scene (except ground and lights) | |
| const clearAllObjects = () => { | |
| if (!scene) return; | |
| scene.meshes.forEach(mesh => { | |
| // Don't remove ground, skybox, or camera/lights | |
| if (mesh.name && | |
| !mesh.name.startsWith('ground') && | |
| !mesh.name.startsWith('skybox') && | |
| !mesh.name.startsWith('camera') && | |
| !mesh.name.startsWith('hemiLight') && | |
| !mesh.name.startsWith('dirLight')) { | |
| mesh.dispose(); | |
| } | |
| }); | |
| hideSelectionInfo(); | |
| currentSelection = null; | |
| }; | |
| // Reset the camera to default position | |
| const resetCamera = () => { | |
| if (!scene || !scene.activeCamera) return; | |
| const camera = scene.activeCamera; | |
| camera.alpha = -Math.PI / 2; | |
| camera.beta = Math.PI / 2; | |
| camera.radius = 15; | |
| camera.target = BABYLON.Vector3.Zero(); | |
| }; | |
| // Take a screenshot of the current scene | |
| const takeScreenshot = () => { | |
| if (!engine) return; | |
| // Create a temporary canvas | |
| const tempCanvas = document.createElement('canvas'); | |
| const tempContext = tempCanvas.getContext('2d'); | |
| // Set canvas size | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| // Copy the contents | |
| tempContext.drawImage(canvas, 0, 0); | |
| // Show the preview | |
| dom.previewImage.src = tempCanvas.toDataURL('image/png'); | |
| dom.screenshotPreview.classList.remove('hidden'); | |
| }; | |
| // Toggle fullscreen mode | |
| const toggleFullscreen = () => { | |
| if (!document.fullscreenElement) { | |
| canvas.requestFullscreen().catch(err => { | |
| console.error(`Error attempting to enable fullscreen: ${err.message}`); | |
| }); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| }; | |
| // ============================================== | |
| // UI Functions | |
| // ============================================== | |
| // Show the selection info panel | |
| const showSelectionInfo = (mesh) => { | |
| dom.selectedName.textContent = mesh.name; | |
| dom.selectedType.textContent = mesh.getClassName(); | |
| dom.selectedPosition.textContent = formatVector(mesh.position); | |
| dom.selectedRotation.textContent = formatVector(mesh.rotation); | |
| dom.selectedScaling.textContent = formatVector(mesh.scaling); | |
| dom.selectedMaterial.textContent = mesh.material ? mesh.material.getClassName() : 'None'; | |
| dom.selectionInfo.classList.remove('hidden'); | |
| }; | |
| // Hide the selection info panel | |
| const hideSelectionInfo = () => { | |
| dom.selectionInfo.classList.add('hidden'); | |
| }; | |
| // Delete the currently selected object | |
| const deleteSelectedObject = () => { | |
| if (!currentSelection) return; | |
| currentSelection.dispose(); | |
| hideSelectionInfo(); | |
| currentSelection = null; | |
| }; | |
| // Update performance stats | |
| const updateStats = () => { | |
| // Throttle the updates | |
| if (!fpsLabel || !objectCountLabel || !scene) return; | |
| // Update FPS | |
| const fps = Math.round(engine.getFps()); | |
| fpsLabel.text = `FPS: ${fps}`; | |
| dom.miniFps.textContent = fps; | |
| // Update object count | |
| const meshes = scene.meshes.filter(m => !m.name.startsWith('_default_')).length; | |
| objectCountLabel.text = `Objects: ${meshes}`; | |
| dom.miniObjCount.textContent = meshes; | |
| // Update draw calls if supported | |
| if (engine.getCurrentRenderCalls) { | |
| dom.miniDrawCalls.textContent = engine.getCurrentRenderCalls(); | |
| } | |
| }; | |
| // ============================================== | |
| // Event Listeners | |
| // ============================================== | |
| // Setup event listeners for UI controls | |
| const setupEventListeners = () => { | |
| // Panel toggle | |
| dom.togglePanel.addEventListener('click', () => { | |
| dom.sidePanel.classList.toggle('-ml-64'); | |
| }); | |
| // Scene management | |
| dom.newScene.addEventListener('click', () => { | |
| clearAllObjects(); | |
| resetCamera(); | |
| }); | |
| dom.saveScene.addEventListener('click', () => { | |
| alert('Scene saved (this would actually save the scene in a real application)'); | |
| }); | |
| // Environment | |
| dom.skyboxSelect.addEventListener('change', (e) => { | |
| setupSkybox(e.target.value); | |
| }); | |
| dom.envIntensity.addEventListener('input', (e) => { | |
| scene.environmentIntensity = parseFloat(e.target.value); | |
| }); | |
| dom.toggleFog.addEventListener('click', () => { | |
| state.environment.fog = !state.environment.fog; | |
| if (state.environment.fog) { | |
| scene.fogMode = BABYLON.Scene.FOGMODE_EXP; | |
| scene.fogDensity = 0.02; | |
| scene.fogColor = new BABYLON.Color3(0.1, 0.1, 0.15); | |
| dom.toggleFog.textContent = 'Disable'; | |
| } else { | |
| scene.fogMode = BABYLON.Scene.FOGMODE_NONE; | |
| dom.toggleFog.textContent = 'Enable'; | |
| } | |
| }); | |
| // Lights | |
| dom.toggleHemiLight.addEventListener('click', () => { | |
| state.lights.hemispheric = !state.lights.hemispheric; | |
| hemisphericLight.intensity = state.lights.hemispheric ? state.lights.intensity : 0; | |
| dom.toggleHemiLight.textContent = state.lights.hemispheric ? 'Disable' : 'Enable'; | |
| }); | |
| dom.toggleDirLight.addEventListener('click', () => { | |
| state.lights.directional = !state.lights.directional; | |
| directionalLight.intensity = state.lights.directional ? state.lights.intensity * 0.8 : 0; | |
| dom.toggleDirLight.textContent = state.lights.directional ? 'Disable' : 'Enable'; | |
| }); | |
| dom.adjustHemiLight.addEventListener('click', () => { | |
| hemisphericLight.diffuse = new BABYLON.Color3( | |
| Math.random(), | |
| Math.random(), | |
| Math.random() | |
| ); | |
| hemisphericLight.groundColor = new BABYLON.Color3( | |
| Math.random(), | |
| Math.random(), | |
| Math.random() | |
| ); | |
| }); | |
| dom.adjustDirLight.addEventListener('click', () => { | |
| directionalLight.direction = new BABYLON.Vector3( | |
| Math.random() * 2 - 1, | |
| Math.random() * -1 - 0.5, | |
| Math.random() * 2 - 1 | |
| ).normalize(); | |
| }); | |
| dom.lightIntensity.addEventListener('input', (e) => { | |
| state.lights.intensity = parseFloat(e.target.value); | |
| hemisphericLight.intensity = state.lights.intensity; | |
| directionalLight.intensity = state.lights.intensity * 0.8; | |
| }); | |
| // Objects | |
| dom.addSphere.addEventListener('click', () => createPrimitive('sphere')); | |
| dom.addBox.addEventListener('click', () => createPrimitive('box')); | |
| dom.addCylinder.addEventListener('click', () => createPrimitive('cylinder')); | |
| dom.addPlane.addEventListener('click', () => createPrimitive('plane')); | |
| dom.clearObjects.addEventListener('click', clearAllObjects); | |
| // Materials | |
| dom.materialType.addEventListener('change', (e) => { | |
| state.currentMaterial = e.target.value; | |
| if (currentSelection) { | |
| applyCurrentMaterialToMesh(currentSelection); | |
| dom.selectedMaterial.textContent = currentSelection.material ? | |
| currentSelection.material.getClassName() : 'None'; | |
| } | |
| }); | |
| const colorButtons = ['red', 'green', 'blue', 'yellow', 'purple', 'gray']; | |
| colorButtons.forEach(color => { | |
| document.getElementById(`color${color.charAt(0).toUpperCase() + color.slice(1)}`) | |
| .addEventListener('click', () => { | |
| state.currentColor = config.materials.colors[color]; | |
| if (currentSelection) { | |
| applyCurrentMaterialToMesh(currentSelection); | |
| } | |
| }); | |
| }); | |
| dom.textureScale.addEventListener('input', (e) => { | |
| if (currentSelection && currentSelection.material) { | |
| const scale = parseFloat(e.target.value); | |
| if (currentSelection.material instanceof BABYLON.GradientMaterial) { | |
| currentSelection.material.scale = scale; | |
| } else if (currentSelection.material instanceof BABYLON.GridMaterial) { | |
| currentSelection.material.scale = scale; | |
| } | |
| } | |
| }); | |
| // Effects | |
| dom.toggleBloom.addEventListener('click', () => { | |
| state.postProcessing.bloom = !state.postProcessing.bloom; | |
| bloomEffect.enabled = state.postProcessing.bloom; | |
| dom.toggleBloom.textContent = state.postProcessing.bloom ? 'Disable' : 'Enable'; | |
| }); | |
| dom.toggleDOF.addEventListener('click', () => { | |
| state.postProcessing.depthOfField = !state.postProcessing.depthOfField; | |
| depthOfFieldEffect.enabled = state.postProcessing.depthOfField; | |
| dom.toggleDOF.textContent = state.postProcessing.depthOfField ? 'Disable' : 'Enable'; | |
| }); | |
| dom.toggleGrain.addEventListener('click', () => { | |
| state.postProcessing.grain = !state.postProcessing.grain; | |
| grainEffect.enabled = state.postProcessing.grain; | |
| dom.toggleGrain.textContent = state.postProcessing.grain ? 'Disable' : 'Enable'; | |
| }); | |
| dom.toggleSSAO.addEventListener('click', () => { | |
| state.postProcessing.ssao = !state.postProcessing.ssao; | |
| ssaoEffect.enabled = state.postProcessing.ssao; | |
| dom.toggleSSAO.textContent = state.postProcessing.ssao ? 'Disable' : 'Enable'; | |
| }); | |
| // Viewport controls | |
| dom.resetCamera.addEventListener('click', resetCamera); | |
| dom.screenshot.addEventListener('click', takeScreenshot); | |
| dom.toggleFullscreen.addEventListener('click', toggleFullscreen); | |
| // Selection info | |
| dom.closeSelection.addEventListener('click', hideSelectionInfo); | |
| dom.deleteSelected.addEventListener('click', deleteSelectedObject); | |
| // Modal controls | |
| dom.continueButton.addEventListener('click', () => { | |
| dom.webgpuModal.classList.add('hidden'); | |
| }); | |
| dom.learnButton.addEventListener('click', () => { | |
| window.open('https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API', '_blank'); | |
| }); | |
| dom.closePreview.addEventListener('click', () => { | |
| dom.screenshotPreview.classList.add('hidden'); | |
| }); | |
| dom.downloadScreenshot.addEventListener('click', () => { | |
| const link = document.createElement('a'); | |
| link.download = `screenshot-${new Date().toISOString()}.png`; | |
| link.href = dom.previewImage.src; | |
| link.click(); | |
| }); | |
| dom.copyScreenshot.addEventListener('click', () => { | |
| navigator.clipboard.writeText(dom.previewImage.src).then(() => { | |
| alert('Screenshot copied to clipboard!'); | |
| }).catch(err => { | |
| console.error('Could not copy screenshot: ', err); | |
| alert('Failed to copy screenshot to clipboard'); | |
| }); | |
| }); | |
| }; | |
| // ============================================== | |
| // Public API | |
| // ============================================== | |
| return { | |
| init: () => initScene(), | |
| getScene: () => scene, | |
| getEngine: () => engine | |
| }; | |
| })(); | |
| // Initialize the application when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| BabylonApp.init(); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> | |
| </html> |