UNPKG

matrix-engine-wgpu

Version:

Networking implemented - based on kurento openvidu server. fix arcball camera,instanced draws added also effect pipeline blend with instancing option.Normalmap added, Fixed shadows casting vs camera/video texture, webGPU powered pwa application. Crazy fas

598 lines (550 loc) 23.9 kB
import {byId, isMobile, jsonHeaders, mb} from "../../engine/utils.js"; /** * @Author NIkola Lukic * @description * Web Editor for matrix-engine-wgpu */ export default class EditorHud { constructor(core) { this.core = core; this.sceneContainer = null; // this.createTopMenu(); this.createTopMenuInFly(); this.createEditorSceneContainer(); this.createScenePropertyBox(); this.currentProperties = []; } createTopMenu() { this.editorMenu = document.createElement("div"); this.editorMenu.id = "editorMenu"; Object.assign(this.editorMenu.style, { position: "absolute", top: "0", left: "20%", width: "60%", height: "50px;", backgroundColor: "rgba(0,0,0,0.85)", display: "flex", alignItems: "start", // overflow: "auto", color: "white", fontFamily: "'Orbitron', sans-serif", zIndex: "15", padding: "2px", boxSizing: "border-box", flexDirection: "row" }); this.editorMenu.innerHTML = " PROJECT MENU "; // document.body.appendChild(this.editorMenu); this.editorMenu.innerHTML = ` <div class="top-item"> <div class="top-btn">Project ▾</div> <div class="dropdown"> <div class="drop-item">📦 Create new project</div> <div class="drop-item">📂 Load</div> <div class="drop-item">💾 Save</div> <div class="drop-item">🛠️ Build</div> </div> </div> <div class="top-item"> <div class="top-btn">Insert ▾</div> <div class="dropdown"> <div class="drop-item">🧊 Cube</div> <div class="drop-item">⚪ Sphere</div> <div class="drop-item">📦 GLB (model)</div> <div class="drop-item">💡 Light</div> </div> </div> <div class="top-item"> <div class="top-btn">View ▾</div> <div class="dropdown"> <div class="drop-item">Hide Editor UI</div> <div class="drop-item">FullScreen</div> </div> </div> <div class="top-item"> <div class="top-btn">About ▾</div> <div class="dropdown"> <div id="showAboutEditor" class="drop-item">matrix-engine-wgpu</div> </div> </div> `; document.body.appendChild(this.editorMenu); // Mobile friendly toggles this.editorMenu.querySelectorAll(".top-btn").forEach(btn => { btn.addEventListener("click", e => { const menu = e.target.nextElementSibling; // close others this.editorMenu.querySelectorAll(".dropdown").forEach(d => { if(d !== menu) d.style.display = "none"; }); // toggle menu.style.display = menu.style.display === "block" ? "none" : "block"; }); }); // Close on outside tap document.addEventListener("click", e => { if(!this.editorMenu.contains(e.target)) { this.editorMenu.querySelectorAll(".dropdown").forEach(d => { d.style.display = "none"; }); } }); this.showAboutModal = () => { alert(` ✔️ Support for 3D objects and scene transformations ✔️ Ammo.js physics full integration ✔️ Networking with Kurento/OpenVidu/Own middleware Nodejs -> frontend 🎯 Replicate matrix-engine (WebGL) features `); } byId('showAboutEditor').addEventListener('click', this.showAboutModal); } createTopMenuInFly() { this.editorMenu = document.createElement("div"); this.editorMenu.id = "editorMenu"; Object.assign(this.editorMenu.style, { position: "absolute", top: "0", left: "20%", width: "60%", height: "50px;", backgroundColor: "rgba(0,0,0,0.85)", display: "flex", alignItems: "start", // overflow: "auto", color: "white", fontFamily: "'Orbitron', sans-serif", zIndex: "15", padding: "2px", boxSizing: "border-box", flexDirection: "row" }); this.editorMenu.innerHTML = " PROJECT MENU "; // document.body.appendChild(this.editorMenu); this.editorMenu.innerHTML = ` <div>INFLY Regime of work no saves. Nice for runtime debugging or get data for map setup.</div> <div class="top-item"> <div class="top-btn">About ▾</div> <div class="dropdown"> <div id="showAboutEditor" class="drop-item">matrix-engine-wgpu</div> </div> </div> `; document.body.appendChild(this.editorMenu); // Mobile friendly toggles this.editorMenu.querySelectorAll(".top-btn").forEach(btn => { btn.addEventListener("click", e => { const menu = e.target.nextElementSibling; // close others this.editorMenu.querySelectorAll(".dropdown").forEach(d => { if(d !== menu) d.style.display = "none"; }); // toggle menu.style.display = menu.style.display === "block" ? "none" : "block"; }); }); // Close on outside tap document.addEventListener("click", e => { if(!this.editorMenu.contains(e.target)) { this.editorMenu.querySelectorAll(".dropdown").forEach(d => { d.style.display = "none"; }); } }); this.showAboutModal = () => { alert(` ✔️ Support for 3D objects and scene transformations ✔️ Ammo.js physics full integration ✔️ Networking with Kurento/OpenVidu/Own middleware Nodejs -> frontend 🎯 Replicate matrix-engine (WebGL) features `); } byId('showAboutEditor').addEventListener('click', this.showAboutModal); } createEditorSceneContainer() { this.sceneContainer = document.createElement("div"); this.sceneContainer.id = "sceneContainer"; Object.assign(this.sceneContainer.style, { position: "absolute", top: "0", left: "0", width: "20%", height: "100vh", backgroundColor: "rgba(0,0,0,0.85)", display: "flex", alignItems: "start", overflow: "auto", color: "white", fontFamily: "'Orbitron', sans-serif", zIndex: "15", padding: "2px", boxSizing: "border-box", flexDirection: "column" }); this.scene = document.createElement("div"); this.scene.id = "scene"; Object.assign(this.scene.style, { width: "100%", height: "100%", backgroundColor: "rgba(0,0,0,0.85)", display: "flex", alignItems: "start", color: "white", fontFamily: "'Orbitron', sans-serif", zIndex: "15", padding: "2px", boxSizing: "border-box", flexDirection: "column" }); this.sceneContainerTitle = document.createElement("div"); this.sceneContainerTitle.style.height = '40px'; this.sceneContainerTitle.style.fontSize = (isMobile() == true ? "x-larger" : "larger"); this.sceneContainerTitle.style.padding = '5px'; this.sceneContainerTitle.innerHTML = 'Scene container'; this.sceneContainer.appendChild(this.sceneContainerTitle); this.sceneContainer.appendChild(this.scene); document.body.appendChild(this.sceneContainer); } updateSceneContainer() { this.scene.innerHTML = ``; this.core.mainRenderBundle.forEach(sceneObj => { let so = document.createElement('div'); so.style.height = '20px'; so.classList.add('sceneContainerItem'); so.innerHTML = sceneObj.name; so.addEventListener('click', this.updateSceneObjProperties); this.scene.appendChild(so); }); } createScenePropertyBox() { this.sceneProperty = document.createElement("div"); this.sceneProperty.id = "sceneProperty"; this.sceneProperty.classList.add('scenePropItem'); Object.assign(this.sceneProperty.style, { position: "absolute", top: "0", right: "0", width: "20%", height: "100vh", backgroundColor: "rgba(0,0,0,0.85)", display: "flex", alignItems: "start", overflow: "auto", color: "white", fontFamily: "'Orbitron', sans-serif", zIndex: "15", padding: "2px", boxSizing: "border-box", flexDirection: "column" }); this.objectProperies = document.createElement("div"); this.objectProperies.id = "objectProperies"; Object.assign(this.objectProperies.style, { width: "100%", height: "auto", backgroundColor: "rgba(0,0,0,0.85)", display: "flex", alignItems: "start", color: "white", fontFamily: "'Orbitron', sans-serif", zIndex: "15", padding: "2px", boxSizing: "border-box", flexDirection: "column" }); this.objectProperiesTitle = document.createElement("div"); this.objectProperiesTitle.style.height = '40px'; this.objectProperiesTitle.style.fontSize = '120%'; this.objectProperiesTitle.innerHTML = 'Scene object properties'; this.sceneProperty.appendChild(this.objectProperiesTitle); this.sceneProperty.appendChild(this.objectProperies); document.body.appendChild(this.sceneProperty); } updateSceneObjProperties = (e) => { this.currentProperties = []; this.objectProperiesTitle.style.fontSize = '120%'; this.objectProperiesTitle.innerHTML = `Scene object properties`; this.objectProperies.innerHTML = ``; const currentSO = this.core.getSceneObjectByName(e.target.innerHTML); this.objectProperiesTitle.innerHTML = `<span style="color:lime;">Name: ${e.target.innerHTML}</span> <span style="color:yellow;"> [${currentSO.constructor.name}]`; const OK = Object.keys(currentSO); OK.forEach((prop) => { // console.log('[key]:', prop); if(prop == 'glb' && typeof currentSO[prop] !== 'undefined' && currentSO[prop] != null) { this.currentProperties.push(new SceneObjectProperty(this.objectProperies, 'glb', currentSO)); } else { this.currentProperties.push(new SceneObjectProperty(this.objectProperies, prop, currentSO)); } }) } } class SceneObjectProperty { constructor(parentDOM, propName, currSceneObj) { this.subObjectsProps = []; this.propName = document.createElement("div"); this.propName.style.width = '100%'; // console.log("init : " + propName) // Register if(propName == "device" || propName == "position" || propName == "rotation" || propName == "raycast" || propName == "entityArgPass" || propName == "scale" || propName == "maxInstances" || propName == "texturesPaths" || propName == "glb" ) { this.propName.style.overflow = 'hidden'; this.propName.style.height = '20px'; this.propName.style.borderBottom = 'solid lime 2px'; this.propName.addEventListener('click', (e) => { if(e.currentTarget.style.height != 'fit-content') { this.propName.style.overflow = 'unset'; e.currentTarget.style.height = 'fit-content'; } else { this.propName.style.overflow = 'hidden'; e.currentTarget.style.height = '20px'; } }) if(propName == "position" || propName == "scale" || propName == "rotation" || propName == "glb") { this.propName.innerHTML = `<div style="text-align:left;" >${propName} <span style="border-radius:7px;background:purple;">sceneObj</span> <span style="border-radius:6px;background:gray;">More info🔽</span></div>`; } else if(propName == "entityArgPass") { this.propName.innerHTML = `<div style="text-align:left;" >${propName} <span style="border-radius:7px;background:brown;">webGPU props</span> <span style="border-radius:6px;background:gray;">More info🔽</span></div>`; } else if(propName == "maxInstances") { this.propName.innerHTML = `<div style="text-align:left;" >${propName} <span style="border-radius:7px;background:brown;">instanced</span> <span style="border-radius:6px;background:gray;"> <input type="number" value="${currSceneObj[propName]}" /> </span></div>`; } else if(propName == "texturesPaths") { this.propName.innerHTML = `<div style="text-align:left;" >${propName} <span style="border-radius:7px;background:purple;">sceneObj</span> <span style="border-radius:6px;background:gray;"> Path: ${currSceneObj[propName]} </span> <div style="text-align:center;padding:5px;margin:5px;"> <img src="${currSceneObj[propName]}" width="256px" height="auto" /> </div> </div>`; } else { this.propName.innerHTML = `<div style="text-align:left;" >${propName} <span style="border-radius:7px;background:red;">sys</span> <span style="border-radius:6px;background:gray;">${currSceneObj[propName]}</span></div>`; } // console.log('[propName] ', propName); if(typeof currSceneObj[propName].adapterInfo !== 'undefined') { this.exploreSubObject(currSceneObj[propName].adapterInfo, 'adapterInfo').forEach((item) => { if(typeof item === 'string') { this.propName.innerHTML += `<div style="text-align:left;"> ${item.split(':'[1])} </div>`; } else { item.addEventListener('click', (event) => { event.stopPropagation(); }); this.propName.appendChild(item); } }) } else if( propName == 'position' || propName == 'rotation' || propName == "raycast" || propName == "entityArgPass" || propName == "scale") { // console.log('currSceneObj[propName] ', currSceneObj[propName]); this.exploreSubObject(currSceneObj[propName], propName, currSceneObj).forEach((item) => { if(typeof item === 'string') { this.propName.innerHTML += `<div style="text-align:left;"> ${item.split(':'[1])} </div>`; } else { item.addEventListener('click', (event) => { event.stopPropagation(); }); this.propName.appendChild(item); } }) } else if(propName == 'glb') { this.exploreGlb(currSceneObj[propName], propName, currSceneObj).forEach((item) => { if(typeof item === 'string') { this.propName.innerHTML += `<div style="text-align:left;"> ${item.split(':'[1])} </div>`; } else { item.addEventListener('click', (event) => { event.stopPropagation(); }); this.propName.appendChild(item); } }); } parentDOM.appendChild(this.propName); } else if(propName == "isVideo") { this.propName.style.borderBottom = 'solid lime 2px'; this.propName.innerHTML = `<div style="text-align:left;" >${propName} <span style="border-radius:7px;background:deepskyblue;">boolean</span> <span style="border-radius:6px;background:gray;">${currSceneObj[propName]}</span></div>`; parentDOM.appendChild(this.propName); } else { // this.propName.innerHTML = `<div>${propName}</div>`; // this.propName.innerHTML += `<div>${currSceneObj[propName]}</div>`; } } exploreSubObject(subobj, rootKey, currSceneObj) { let a = []; let __ = []; for(const key in subobj) { __.push(key); } __.forEach((prop, index) => { if(index == 0) a.push(`${rootKey}`); let d = null; d = document.createElement("div"); d.style.textAlign = "left"; d.style.display = "flex"; if(typeof subobj[prop] === 'number') { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:48%; background:lime;color:black;" > <input class="inputEditor" name="${prop}" onchange="console.log(this.value, 'change fired'); document.dispatchEvent(new CustomEvent('web.editor.input', {detail: { 'inputFor': ${currSceneObj ? "'" + currSceneObj.name + "'" : "'no info'"} , 'propertyId': ${currSceneObj ? "'" + rootKey + "'" : "'no info'"} , 'property': ${currSceneObj ? "'" + prop + "'" : "'no info'"} , 'value': ${currSceneObj ? "this.value" : "'no info'"} }}))" ${(rootKey == "adapterInfo" ? " disabled='true'" : " ")} type="number" value="${subobj[prop]}" /> </div>`; } else if(Array.isArray(subobj[prop])) { d.innerHTML += `<div style="width:50%">${prop}</div> <div style="width:${(subobj[prop].length == 0 ? "unset" : "48%")}; background:lime;color:black;border-radius:5px;" > ${(subobj[prop].length == 0 ? "[Empty array]" : subobj[prop])} </div>`; } else if(subobj[prop] === null) { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:unset; background:lime;color:black;padding:1px;border-radius:5px;" >${subobj[prop]}</div>`; } else if(subobj[prop] == false) { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:unset; background:lime;color:black;padding:1px;border-radius:5px;" >false</div>`; } else if(subobj[prop] == "") { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:unset; background:lime;color:black;padding:1px;border-radius:5px;" >none</div>`; } else { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:48%; background:lime;color:black;padding:1px;border-radius:5px;" >${subobj[prop]}</div>`; } a.push(d); }); // this.subObjectsProps.push(a); return a; } exploreGlb(subobj, rootKey, currSceneObj) { let a = []; let __ = []; for(const key in subobj) { __.push(key); } __.forEach((prop, index) => { if(index == 0) a.push(`${rootKey}`); let d = null; d = document.createElement("div"); d.style.textAlign = "left"; d.style.display = "flex"; d.style.flexWrap = "wrap"; if(typeof subobj[prop] === 'number') { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:48%; background:lime;color:black;" > <input class="inputEditor" name="${prop}" ${(prop === "animationIndex" ? "max='" + subobj['glbJsonData']['animations'].length - 1 + "'" : "")} onchange="console.log(this.value, 'change fired'); document.dispatchEvent(new CustomEvent('web.editor.input', {detail: { 'inputFor': ${currSceneObj ? "'" + currSceneObj.name + "'" : "'no info'"} , 'propertyId': ${currSceneObj ? "'" + rootKey + "'" : "'no info'"} , 'property': ${currSceneObj ? "'" + prop + "'" : "'no info'"} , 'value': ${currSceneObj ? "this.value" : "'no info'"} }}))" ${(rootKey == "adapterInfo" ? "disabled='true'" : "")}" type="number" value="${subobj[prop]}" /> </div>`; } else if(Array.isArray(subobj[prop]) && prop == "nodes") { console.log("init prop: " + rootKey) d.innerHTML += `<div style="width:50%">${prop}</div> <div style="width:${(subobj[prop].length == 0 ? "unset" : "48%")}; background:lime;color:black;border-radius:5px;" > ${(subobj[prop].length == 0 ? "[Empty array]" : subobj[prop].length)} </div>`; } else if(Array.isArray(subobj[prop]) && prop == "skins") { console.log("init prop: " + rootKey) d.innerHTML += `<div style="width:50%">${prop}</div> <div style="width:${(subobj[prop].length == 0 ? "unset" : "48%")}; background:lime;color:black;border-radius:5px;" > ${(subobj[prop].length == 0 ? "[Empty array]" : subobj[prop] .map(item => { if(item && typeof item === "object" && "name" in item) { return item.name + " Joints:" + item.joints.length + "\n inverseBindMatrices:" + item.inverseBindMatrices; } return String(item); }) .join(", ") )} </div>`; } else if(prop == "glbJsonData") { console.log("init glbJsonData: " + rootKey) d.innerHTML += `<div style="width:50%">Animations:</div> <div style="width:${(subobj[prop].animations.length == 0 ? "unset" : "48%")}; background:lime;color:black;border-radius:5px;" > ${(subobj[prop].animations.length == 0 ? "[Empty array]" : subobj[prop].animations .map(item => { if(item && typeof item === "object" && "name" in item) { return item.name; } return String(item); }) .join(", ") )} </div> \n <div style="width:50%">Skinned meshes:</div> <div style="width:${(subobj[prop].meshes.length == 0 ? "unset" : "48%")}; background:lime;color:black;border-radius:5px;" > ${(subobj[prop].meshes.length == 0 ? "[Empty array]" : subobj[prop].meshes .map(item => { if(item && typeof item === "object" && "name" in item) { return item.name + " \n Primitives : " + item.primitives.length; } return String(item); }) .join(", ") )} </div> \n <div style="width:50%">Images:</div> <div style="width:${(subobj[prop].images.length == 0 ? "unset" : "48%")}; background:lime;color:black;border-radius:5px;" > ${(subobj[prop].images.length == 0 ? "[Empty array]" : subobj[prop].images .map(item => { if(item && typeof item === "object" && "name" in item) { return "<div>" + item.name + " \n mimeType: " + item.mimeType + "</div>"; } return String(item); }) .join(", ") )} </div> \n <div style="width:50%">Materials:</div> <div style="width:${(subobj[prop].materials.length == 0 ? "unset" : "48%")}; background:lime;color:black;border-radius:5px;" > ${(subobj[prop].materials.length == 0 ? "[Empty array]" : subobj[prop].materials .map(item => { if(item && typeof item === "object" && "name" in item) { return "<div>" + item.name + " \n metallicFactor: " + item.pbrMetallicRoughness.metallicFactor + " \n roughnessFactor: " + item.pbrMetallicRoughness.roughnessFactor + "</div>"; } return String(item); }) .join(", ") )} </div> `; } else if(subobj[prop] === null) { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:unset; background:lime;color:black;padding:1px;border-radius:5px;" >${subobj[prop]}</div>`; } else if(subobj[prop] == false) { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:unset; background:lime;color:black;padding:1px;border-radius:5px;" >false</div>`; } else if(subobj[prop] == "") { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:unset; background:lime;color:black;padding:1px;border-radius:5px;" >none</div>`; } else { d.innerHTML += `<div style="width:50%;">${prop}</div> <div style="width:48%; background:lime;color:black;padding:1px;border-radius:5px;" >${subobj[prop]}</div>`; } a.push(d); }); // this.subObjectsProps.push(a); return a; } }