1276 lines
34 KiB
JavaScript
1276 lines
34 KiB
JavaScript
|
|
import * as THREE from 'https://esm.sh/three@0.160.0';
|
||
|
|
import { OrbitControls } from 'https://esm.sh/three@0.160.0/examples/jsm/controls/OrbitControls.js';
|
||
|
|
import { TransformControls } from 'https://esm.sh/three@0.160.0/examples/jsm/controls/TransformControls.js';
|
||
|
|
import { EffectComposer } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js';
|
||
|
|
import { RenderPass } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/RenderPass.js';
|
||
|
|
import { SSAOPass } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/SSAOPass.js';
|
||
|
|
|
||
|
|
class TextureEditorApp {
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
|
||
|
|
this.scene = new THREE.Scene();
|
||
|
|
this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
|
||
|
|
const canvas = document.getElementById('threeCanvas');
|
||
|
|
this.renderer = new THREE.WebGLRenderer({
|
||
|
|
canvas: canvas,
|
||
|
|
antialias: true
|
||
|
|
});
|
||
|
|
this.composer = true;
|
||
|
|
this.ssaoPass = null;
|
||
|
|
this.controls = null;
|
||
|
|
this.transformControls = null;
|
||
|
|
this.pointLight = null;
|
||
|
|
this.raycaster = new THREE.Raycaster();
|
||
|
|
this.mouse = new THREE.Vector2();
|
||
|
|
this.selectable = [];
|
||
|
|
this.draggableMeshes = [];
|
||
|
|
this.sceneObjects = [];
|
||
|
|
this.selectedObject = null;
|
||
|
|
|
||
|
|
this.materials = [];
|
||
|
|
this.selectedMaterialIndex = -1;
|
||
|
|
|
||
|
|
this.init().then(() => {
|
||
|
|
this.animate();
|
||
|
|
});
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
async init() {
|
||
|
|
this.cacheUI();
|
||
|
|
|
||
|
|
|
||
|
|
await this.loadMaterialsFromStorage();
|
||
|
|
|
||
|
|
this.loadPrimitivesFromStorage();
|
||
|
|
this.initCamera();
|
||
|
|
this.initRenderer();
|
||
|
|
this.initLights();
|
||
|
|
this.initControls();
|
||
|
|
this.initPostProcessing();
|
||
|
|
this.initTransformControls();
|
||
|
|
this.initThumbnailPreview();
|
||
|
|
|
||
|
|
this.addEventListeners();
|
||
|
|
this.addRoom();
|
||
|
|
//this.addBigBox();
|
||
|
|
|
||
|
|
this.initMaterialPreview();
|
||
|
|
this.refreshMaterialList();
|
||
|
|
|
||
|
|
|
||
|
|
this.stabilizeResize();
|
||
|
|
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
stabilizeResize(attempts = 10) {
|
||
|
|
if (attempts <= 0) return;
|
||
|
|
|
||
|
|
this.onResize();
|
||
|
|
|
||
|
|
requestAnimationFrame(() => this.stabilizeResize(attempts - 1));
|
||
|
|
}
|
||
|
|
updatePreviewMaterial(mat) {
|
||
|
|
if (this.previewSphere) {
|
||
|
|
this.previewSphere.material = mat;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
initMaterialPreview() {
|
||
|
|
this.previewScene = new THREE.Scene();
|
||
|
|
this.previewCamera = new THREE.PerspectiveCamera(45, 1, 0.1, 10);
|
||
|
|
this.previewRenderer = new THREE.WebGLRenderer({
|
||
|
|
canvas: document.getElementById('materialPreview'),
|
||
|
|
alpha: true,
|
||
|
|
antialias: true
|
||
|
|
});
|
||
|
|
this.previewRenderer.setSize(200, 200);
|
||
|
|
this.previewRenderer.setClearColor(0x000000, 0);
|
||
|
|
|
||
|
|
|
||
|
|
//this.previewRenderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||
|
|
//this.previewRenderer.toneMappingExposure = 1.3;
|
||
|
|
|
||
|
|
|
||
|
|
this.previewLight = new THREE.PointLight(0xffffff, 2);
|
||
|
|
this.previewLight.position.set(2, 2, 2);
|
||
|
|
this.previewScene.add(this.previewLight);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
this.previewSphere = new THREE.Mesh(
|
||
|
|
new THREE.SphereGeometry(0.6, 32, 32),
|
||
|
|
new THREE.MeshStandardMaterial({ color: 0xffffff })
|
||
|
|
);
|
||
|
|
this.previewScene.add(this.previewSphere);
|
||
|
|
|
||
|
|
this.previewCamera.position.z = 2;
|
||
|
|
this.previewCamera.lookAt(0, 0, 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
async loadTexture(loader, url) {
|
||
|
|
|
||
|
|
const texture = await new Promise((resolve) => {
|
||
|
|
loader.load(url, (tex) => {
|
||
|
|
// Check if it's a normal map based on filename
|
||
|
|
const isNormal = url.toLowerCase().includes('normal');
|
||
|
|
|
||
|
|
if (isNormal) {
|
||
|
|
tex.encoding = THREE.LinearEncoding;
|
||
|
|
tex.minFilter = THREE.LinearMipMapLinearFilter;
|
||
|
|
tex.magFilter = THREE.LinearFilter;
|
||
|
|
}
|
||
|
|
|
||
|
|
tex.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
|
||
|
|
|
||
|
|
resolve(tex);
|
||
|
|
}, undefined, () => resolve(null));
|
||
|
|
});
|
||
|
|
|
||
|
|
return texture;
|
||
|
|
}
|
||
|
|
|
||
|
|
async loadMaterialsFromStorage() {
|
||
|
|
|
||
|
|
const data = localStorage.getItem("materials");
|
||
|
|
|
||
|
|
if (!data) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const list = JSON.parse(data);
|
||
|
|
|
||
|
|
const loader = new THREE.TextureLoader();
|
||
|
|
|
||
|
|
for (const item of list) {
|
||
|
|
|
||
|
|
const material = new THREE.MeshStandardMaterial({
|
||
|
|
color: new THREE.Color(item.color),
|
||
|
|
metalness: item.metalness,
|
||
|
|
roughness: item.roughness,
|
||
|
|
displacementScale: item.displacementScale,
|
||
|
|
opacity: item.opacity,
|
||
|
|
transparent: item.opacity < 1.0
|
||
|
|
});
|
||
|
|
|
||
|
|
if (item.mapURL) {
|
||
|
|
material.map = await this.loadTexture(loader, item.mapURL);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (item.normalMapURL) {
|
||
|
|
|
||
|
|
const normalMap = await this.loadTexture(loader, item.normalMapURL);
|
||
|
|
|
||
|
|
if (normalMap) {
|
||
|
|
|
||
|
|
normalMap.wrapS = THREE.RepeatWrapping;
|
||
|
|
|
||
|
|
normalMap.wrapT = THREE.RepeatWrapping;
|
||
|
|
|
||
|
|
normalMap.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
|
||
|
|
|
||
|
|
normalMap.generateMipmaps = true;
|
||
|
|
|
||
|
|
normalMap.minFilter = THREE.LinearMipMapLinearFilter;
|
||
|
|
|
||
|
|
normalMap.magFilter = THREE.LinearFilter;
|
||
|
|
|
||
|
|
normalMap.encoding = THREE.LinearEncoding;
|
||
|
|
|
||
|
|
normalMap.needsUpdate = true;
|
||
|
|
|
||
|
|
material.normalMap = normalMap;
|
||
|
|
|
||
|
|
material.normalScale = new THREE.Vector2( 0.5, 0.5 );
|
||
|
|
|
||
|
|
} else {
|
||
|
|
|
||
|
|
console.warn( "Failed to load normal map:", item.normalMapURL );
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
}
|
||
|
|
if (item.displacementMapURL) {
|
||
|
|
|
||
|
|
const displacementMap = await this.loadTexture(loader, item.displacementMapURL);
|
||
|
|
|
||
|
|
if (displacementMap) {
|
||
|
|
|
||
|
|
displacementMap.wrapS = THREE.RepeatWrapping;
|
||
|
|
displacementMap.wrapT = THREE.RepeatWrapping;
|
||
|
|
displacementMap.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
|
||
|
|
displacementMap.minFilter = THREE.LinearMipMapLinearFilter;
|
||
|
|
displacementMap.magFilter = THREE.LinearFilter;
|
||
|
|
displacementMap.generateMipmaps = true;
|
||
|
|
displacementMap.needsUpdate = true;
|
||
|
|
|
||
|
|
material.displacementMap = displacementMap;
|
||
|
|
material.displacementScale = item.displacementScale ?? 0.03;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
if (item.roughnessMapURL) {
|
||
|
|
material.roughnessMap = await this.loadTexture(loader, item.roughnessMapURL);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (item.metalnessMapURL) {
|
||
|
|
material.metalnessMap = await this.loadTexture(loader, item.metalnessMapURL);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (item.alphaMapURL) {
|
||
|
|
material.alphaMap = await this.loadTexture(loader, item.alphaMapURL);
|
||
|
|
material.transparent = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
material.needsUpdate = true;
|
||
|
|
|
||
|
|
this.materials.push({
|
||
|
|
name: item.name,
|
||
|
|
material: material,
|
||
|
|
mapURL: item.mapURL,
|
||
|
|
normalMapURL: item.normalMapURL,
|
||
|
|
roughnessMapURL: item.roughnessMapURL,
|
||
|
|
metalnessMapURL: item.metalnessMapURL,
|
||
|
|
alphaMapURL: item.alphaMapURL
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
this.refreshMaterialList();
|
||
|
|
}
|
||
|
|
saveMaterialsToStorage(mat) {
|
||
|
|
|
||
|
|
this.updatePreviewMaterial(mat);
|
||
|
|
|
||
|
|
const list = this.materials.map(({ name, material, mapURL, normalMapURL, roughnessMapURL, metalnessMapURL, alphaMapURL, displacementMapURL }) => {
|
||
|
|
|
||
|
|
const obj = {
|
||
|
|
|
||
|
|
name: name,
|
||
|
|
color: "#" + material.color.getHexString(),
|
||
|
|
metalness: material.metalness,
|
||
|
|
roughness: material.roughness,
|
||
|
|
displacementScale: material.displacementScale,
|
||
|
|
opacity: material.opacity,
|
||
|
|
mapURL: mapURL || null,
|
||
|
|
normalMapURL: normalMapURL || null,
|
||
|
|
roughnessMapURL: roughnessMapURL || null,
|
||
|
|
metalnessMapURL: metalnessMapURL || null,
|
||
|
|
alphaMapURL: alphaMapURL || null,
|
||
|
|
displacementMapURL: displacementMapURL || null
|
||
|
|
|
||
|
|
};
|
||
|
|
|
||
|
|
return obj;
|
||
|
|
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log("saved material count:", list.length);
|
||
|
|
|
||
|
|
localStorage.setItem("materials", JSON.stringify(list));
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
refreshMaterialList() {
|
||
|
|
this.matList.innerHTML = '';
|
||
|
|
|
||
|
|
this.materials.forEach(async (entry, index) => {
|
||
|
|
const div = document.createElement('div');
|
||
|
|
div.style.padding = '5px';
|
||
|
|
div.style.marginBottom = '10px';
|
||
|
|
div.style.border = '1px solid #333';
|
||
|
|
div.style.background = '#111';
|
||
|
|
div.style.color = '#fff';
|
||
|
|
div.style.cursor = 'grab';
|
||
|
|
div.draggable = true;
|
||
|
|
div.dataset.index = index;
|
||
|
|
|
||
|
|
const img = document.createElement('img');
|
||
|
|
img.width = 216;
|
||
|
|
img.height = 216;
|
||
|
|
img.style.display = 'block';
|
||
|
|
img.style.margin = '0 auto 5px auto';
|
||
|
|
img.src = ''; // set later
|
||
|
|
|
||
|
|
const label = document.createElement('div');
|
||
|
|
label.textContent = entry.name;
|
||
|
|
label.style.textAlign = 'center';
|
||
|
|
|
||
|
|
div.appendChild(img);
|
||
|
|
div.appendChild(label);
|
||
|
|
|
||
|
|
div.addEventListener('dragstart', (e) => {
|
||
|
|
e.dataTransfer.setData('materialIndex', index);
|
||
|
|
});
|
||
|
|
|
||
|
|
div.addEventListener('click', () => {
|
||
|
|
this.matNameInput.value = entry.name;
|
||
|
|
this.colorInput.value = '#' + entry.material.color.getHexString();
|
||
|
|
this.selectedMaterialIndex = index;
|
||
|
|
|
||
|
|
this.updatePreviewMaterial(entry.material);
|
||
|
|
|
||
|
|
if (this.selectedObject) {
|
||
|
|
this.selectedObject.material = entry.material;
|
||
|
|
this.attachMaterialControls(entry.material);
|
||
|
|
this.updateUIFromMaterial(entry.material);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.matList.appendChild(div);
|
||
|
|
|
||
|
|
// Render preview AFTER textures are ready
|
||
|
|
const dataURL = await this.renderThumbnail(entry.material);
|
||
|
|
img.src = dataURL;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
adjustImageBrightness(img, brightnessFactor = 0.75) {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
const canvas = document.createElement('canvas');
|
||
|
|
canvas.width = img.width;
|
||
|
|
canvas.height = img.height;
|
||
|
|
const ctx = canvas.getContext('2d');
|
||
|
|
|
||
|
|
img.onload = () => {
|
||
|
|
ctx.drawImage(img, 0, 0);
|
||
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||
|
|
const data = imageData.data;
|
||
|
|
|
||
|
|
for (let i = 0; i < data.length; i += 4) {
|
||
|
|
data[i] *= brightnessFactor; // R
|
||
|
|
data[i + 1] *= brightnessFactor; // G
|
||
|
|
data[i + 2] *= brightnessFactor; // B
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.putImageData(imageData, 0, 0);
|
||
|
|
resolve(canvas.toDataURL());
|
||
|
|
};
|
||
|
|
|
||
|
|
img.src = img.src; // trigger reload
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async waitForTextures(material) {
|
||
|
|
const maps = ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'alphaMap'];
|
||
|
|
const promises = [];
|
||
|
|
|
||
|
|
maps.forEach(key => {
|
||
|
|
const tex = material[key];
|
||
|
|
|
||
|
|
if (tex) {
|
||
|
|
if (tex.image) {
|
||
|
|
// If image exists and is complete, resolve immediately
|
||
|
|
if (tex.image.complete || (tex.image.readyState && tex.image.readyState === 4)) {
|
||
|
|
// already loaded
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Listen for load event if image exists but not loaded
|
||
|
|
promises.push(
|
||
|
|
new Promise(resolve => {
|
||
|
|
tex.image.addEventListener('load', () => resolve());
|
||
|
|
tex.image.addEventListener('error', () => resolve());
|
||
|
|
})
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
// If no image (likely a DataTexture or procedural), wait for onUpdate callback
|
||
|
|
promises.push(
|
||
|
|
new Promise(resolve => {
|
||
|
|
tex.onUpdate = () => {
|
||
|
|
resolve();
|
||
|
|
tex.onUpdate = null;
|
||
|
|
};
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await Promise.all(promises);
|
||
|
|
}
|
||
|
|
initThumbnailPreview() {
|
||
|
|
this.thumbRenderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
|
||
|
|
this.thumbRenderer.setSize( 256, 256 );
|
||
|
|
this.thumbRenderer.setClearColor(0x000000, 0);
|
||
|
|
|
||
|
|
this.thumbScene = new THREE.Scene();
|
||
|
|
|
||
|
|
this.thumbCamera = new THREE.PerspectiveCamera(45, 1, 0.1, 10);
|
||
|
|
this.thumbCamera.position.z = 2;
|
||
|
|
this.thumbCamera.lookAt(0, 0, 0);
|
||
|
|
|
||
|
|
const light = new THREE.DirectionalLight(0xffffff, 1.5);
|
||
|
|
light.position.set(4, 4, 4);
|
||
|
|
this.thumbScene.add(light);
|
||
|
|
|
||
|
|
// Add ambient light to soften shadows & prevent complete black areas
|
||
|
|
this.thumbScene.add(new THREE.AmbientLight(0xffffff, 0.3));
|
||
|
|
|
||
|
|
this.thumbComposer = new EffectComposer(this.thumbRenderer);
|
||
|
|
this.thumbComposer.addPass(new RenderPass(this.thumbScene, this.thumbCamera));
|
||
|
|
|
||
|
|
this.thumbSsaoPass = new SSAOPass(this.thumbScene, this.thumbCamera, 64, 64);
|
||
|
|
this.thumbSsaoPass.kernelRadius = 6;
|
||
|
|
this.thumbSsaoPass.minDistance = 0.02;
|
||
|
|
this.thumbSsaoPass.maxDistance = 0.15;
|
||
|
|
this.thumbComposer.addPass(this.thumbSsaoPass);
|
||
|
|
}
|
||
|
|
|
||
|
|
async renderThumbnail(material) {
|
||
|
|
// Clone the material to avoid modifying the original
|
||
|
|
const mat = material.clone();
|
||
|
|
|
||
|
|
// Ensure all texture maps are loaded
|
||
|
|
await this.waitForTextures(mat);
|
||
|
|
|
||
|
|
// Mark material for update after textures are loaded
|
||
|
|
mat.needsUpdate = true;
|
||
|
|
|
||
|
|
// Remove old mesh if any
|
||
|
|
if (this.thumbMesh) {
|
||
|
|
this.thumbScene.remove(this.thumbMesh);
|
||
|
|
this.thumbMesh.geometry.dispose();
|
||
|
|
this.thumbMesh.material.dispose();
|
||
|
|
this.thumbMesh = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Recreate mesh after textures are loaded and material is valid
|
||
|
|
this.thumbMesh = new THREE.Mesh(new THREE.SphereGeometry(0.6, 32, 32), mat);
|
||
|
|
this.thumbScene.add(this.thumbMesh);
|
||
|
|
|
||
|
|
// Force material update in renderer
|
||
|
|
this.thumbRenderer.outputColorSpace = THREE.SRGBColorSpace;
|
||
|
|
this.thumbRenderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||
|
|
this.thumbRenderer.toneMappingExposure = 1.3;
|
||
|
|
this.thumbRenderer.physicallyCorrectLights = true;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
this.thumbRenderer.clear();
|
||
|
|
|
||
|
|
this.thumbComposer.render();
|
||
|
|
|
||
|
|
// Return snapshot
|
||
|
|
const dataURL = this.thumbRenderer.domElement.toDataURL();
|
||
|
|
|
||
|
|
// Remove mesh again if this is purely for snapshot (optional)
|
||
|
|
this.thumbScene.remove(this.thumbMesh);
|
||
|
|
this.thumbMesh.geometry.dispose();
|
||
|
|
this.thumbMesh.material.dispose();
|
||
|
|
this.thumbMesh = null;
|
||
|
|
|
||
|
|
return dataURL;
|
||
|
|
}
|
||
|
|
promptAddMaterial() {
|
||
|
|
|
||
|
|
const matName = prompt("Enter new material name:");
|
||
|
|
|
||
|
|
if (!matName || matName.trim() === "") return;
|
||
|
|
|
||
|
|
const name = matName.trim();
|
||
|
|
|
||
|
|
const material = new THREE.MeshStandardMaterial({ color: 0xffffff });
|
||
|
|
|
||
|
|
this.materials.push({ name: name, material: material });
|
||
|
|
|
||
|
|
this.selectedMaterialIndex = this.materials.length - 1;
|
||
|
|
|
||
|
|
this.matNameInput.value = name;
|
||
|
|
|
||
|
|
this.updatePreviewMaterial(material);
|
||
|
|
|
||
|
|
this.refreshMaterialList();
|
||
|
|
|
||
|
|
this.saveMaterialsToStorage(material);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
cacheUI() {
|
||
|
|
/*
|
||
|
|
this.loadMaterialFromFolder("Marble002_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Terrazzo015_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("DiamondPlate001_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Marble003_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Tiles040_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("DiamondPlate005B_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("PavingStones072_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Tiles041_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("DiamondPlate005D_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("PavingStones075_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Tiles044_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Gravel022_1K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("PavingStones085_2K-JPG.usdc", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Tiles105_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Tiles108_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Gravel023_1K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Plastic012A_1K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("WoodFloor046_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Leather034A_2K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Plastic015A_1K-JPG", "/textures");
|
||
|
|
|
||
|
|
this.loadMaterialFromFolder("Leather034C_2K-JPG", "/textures");
|
||
|
|
*/
|
||
|
|
this.matNameInput = document.getElementById("matName");
|
||
|
|
this.saveMaterialBtn = document.getElementById("saveMaterial"); // existing save button
|
||
|
|
this.addMaterialBtn = document.getElementById("addMaterialBtn"); // new add button
|
||
|
|
this.matList = document.getElementById("materialList");
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
this.colorInput = document.getElementById('colorPicker');
|
||
|
|
this.roughnessInput = document.getElementById('roughnessSlider');
|
||
|
|
this.metalnessInput = document.getElementById('metalnessSlider');
|
||
|
|
this.displacementInput = document.getElementById('displacementSlider');
|
||
|
|
this.opacityInput = document.getElementById('opacitySlider');
|
||
|
|
|
||
|
|
this.inputs = {
|
||
|
|
map: document.getElementById('mapInput'),
|
||
|
|
normalMap: document.getElementById('normalInput'),
|
||
|
|
roughnessMap: document.getElementById('roughnessInput'),
|
||
|
|
metalnessMap: document.getElementById('metalnessInput'),
|
||
|
|
alphaMap: document.getElementById('alphaInput')
|
||
|
|
};
|
||
|
|
|
||
|
|
this.saveMaterialBtn.addEventListener('click', () => this.addOrUpdateMaterial());
|
||
|
|
|
||
|
|
if (this.addMaterialBtn) {
|
||
|
|
this.addMaterialBtn.addEventListener("click", () => this.promptAddMaterial());
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
for (const mapType in this.inputs) {
|
||
|
|
|
||
|
|
|
||
|
|
this.inputs[mapType].addEventListener('change', (e) => {
|
||
|
|
if (e.target.files.length > 0) {
|
||
|
|
this.applyTexture(mapType, e.target.files[0]);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.inputs[mapType].addEventListener('dragover', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
});
|
||
|
|
|
||
|
|
this.inputs[mapType].addEventListener('drop', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const file = e.dataTransfer.files[0];
|
||
|
|
if (file) {
|
||
|
|
this.applyTexture(mapType, file);
|
||
|
|
this.inputs[mapType].files = e.dataTransfer.files; // Optional: visually update the input
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
this.colorInput.addEventListener('change', () => {
|
||
|
|
if (this.selectedMaterialIndex >= 0) {
|
||
|
|
const mat = this.materials[this.selectedMaterialIndex].material;
|
||
|
|
mat.color.set(this.colorInput.value);
|
||
|
|
|
||
|
|
|
||
|
|
this.refreshMaterialList();
|
||
|
|
this.saveMaterialsToStorage(mat); // ✅ save after color change
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.roughnessInput.addEventListener('change', () => {
|
||
|
|
if (this.selectedMaterialIndex >= 0) {
|
||
|
|
const mat = this.materials[this.selectedMaterialIndex].material;
|
||
|
|
mat.roughness = parseFloat(this.roughnessInput.value);
|
||
|
|
this.saveMaterialsToStorage(mat); // ✅ save after change
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.metalnessInput.addEventListener('change', () => {
|
||
|
|
if (this.selectedMaterialIndex >= 0) {
|
||
|
|
const mat = this.materials[this.selectedMaterialIndex].material;
|
||
|
|
mat.metalness = parseFloat(this.metalnessInput.value);
|
||
|
|
this.saveMaterialsToStorage(mat); // ✅ save after change
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.displacementInput.addEventListener('change', () => {
|
||
|
|
if (this.selectedMaterialIndex >= 0) {
|
||
|
|
const mat = this.materials[this.selectedMaterialIndex].material;
|
||
|
|
mat.displacementScale = parseFloat(this.displacementInput.value);
|
||
|
|
this.saveMaterialsToStorage(mat); // ✅ save after change
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.opacityInput.addEventListener('change', () => {
|
||
|
|
if (this.selectedMaterialIndex >= 0) {
|
||
|
|
const mat = this.materials[this.selectedMaterialIndex].material;
|
||
|
|
mat.opacity = parseFloat(this.opacityInput.value);
|
||
|
|
mat.transparent = mat.opacity < 1;
|
||
|
|
this.saveMaterialsToStorage(mat); // ✅ save after change
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
this.primitiveList = document.getElementById('primitiveList');
|
||
|
|
|
||
|
|
Array.from(this.primitiveList.querySelectorAll('[draggable="true"]')).forEach((el) => {
|
||
|
|
el.addEventListener('dragstart', (e) => {
|
||
|
|
e.dataTransfer.setData('primitiveType', el.dataset.primitive);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
document.body.addEventListener('dragover', (e) => e.preventDefault());
|
||
|
|
document.body.addEventListener('drop', this.onDropMaterial.bind(this));
|
||
|
|
}
|
||
|
|
|
||
|
|
savePrimitivesToStorage() {
|
||
|
|
this.primitivesData = [];
|
||
|
|
console.log("save transform");
|
||
|
|
this.sceneObjects.forEach((obj) => {
|
||
|
|
|
||
|
|
if (!obj.userData.isPrimitive) return;
|
||
|
|
|
||
|
|
const data = {
|
||
|
|
type: obj.userData.type,
|
||
|
|
position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
|
||
|
|
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z },
|
||
|
|
scale: { x: obj.scale.x, y: obj.scale.y, z: obj.scale.z },
|
||
|
|
materialName: null
|
||
|
|
};
|
||
|
|
|
||
|
|
// Find material name from your materials list
|
||
|
|
for (let i = 0; i < this.materials.length; i++) {
|
||
|
|
if (obj.material === this.materials[i].material) {
|
||
|
|
data.materialName = this.materials[i].name;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
this.primitivesData.push(data);
|
||
|
|
});
|
||
|
|
console.log(this.primitivesData);
|
||
|
|
localStorage.setItem('primitives', JSON.stringify(this.primitivesData));
|
||
|
|
}
|
||
|
|
|
||
|
|
loadPrimitivesFromStorage() {
|
||
|
|
const saved = localStorage.getItem('primitives');
|
||
|
|
if (!saved) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const list = JSON.parse(saved);
|
||
|
|
|
||
|
|
list.forEach((data) => {
|
||
|
|
|
||
|
|
let geometry = null;
|
||
|
|
|
||
|
|
switch(data.type) {
|
||
|
|
case 'cube':
|
||
|
|
geometry = new THREE.BoxGeometry(1, 1, 1);
|
||
|
|
break;
|
||
|
|
case 'sphere':
|
||
|
|
geometry = new THREE.SphereGeometry(0.5, 32, 32);
|
||
|
|
break;
|
||
|
|
case 'cone':
|
||
|
|
geometry = new THREE.ConeGeometry(0.5, 1, 32);
|
||
|
|
break;
|
||
|
|
case 'cone':
|
||
|
|
geometry = new THREE.ConeGeometry(0.5, 1, 32);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'cylinder':
|
||
|
|
geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32);
|
||
|
|
break;
|
||
|
|
|
||
|
|
|
||
|
|
// Add more types if you want
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!geometry) return;
|
||
|
|
|
||
|
|
// Find material by name or fallback to default
|
||
|
|
let mat = this.materials.find(m => m.name === data.materialName);
|
||
|
|
if (mat) {
|
||
|
|
mat = mat.material;
|
||
|
|
} else {
|
||
|
|
mat = new THREE.MeshStandardMaterial({ color: '#888888' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const mesh = new THREE.Mesh(geometry, mat);
|
||
|
|
|
||
|
|
mesh.position.set(data.position.x, data.position.y, data.position.z);
|
||
|
|
mesh.rotation.set(data.rotation.x, data.rotation.y, data.rotation.z);
|
||
|
|
mesh.scale.set(data.scale.x, data.scale.y, data.scale.z);
|
||
|
|
|
||
|
|
mesh.castShadow = true;
|
||
|
|
mesh.receiveShadow = true;
|
||
|
|
|
||
|
|
mesh.userData.isPrimitive = true;
|
||
|
|
mesh.userData.type = data.type;
|
||
|
|
|
||
|
|
this.scene.add(mesh);
|
||
|
|
this.selectable.push(mesh);
|
||
|
|
this.sceneObjects.push(mesh);
|
||
|
|
});
|
||
|
|
|
||
|
|
} catch (e) {
|
||
|
|
console.warn('Failed to load primitives', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onDropPrimitive(event) {
|
||
|
|
|
||
|
|
event.preventDefault();
|
||
|
|
console.log("drop primitive");
|
||
|
|
const primitiveType = event.dataTransfer.getData('primitiveType');
|
||
|
|
|
||
|
|
if (!primitiveType) return;
|
||
|
|
|
||
|
|
const canvas = this.renderer.domElement;
|
||
|
|
const rect = canvas.getBoundingClientRect();
|
||
|
|
|
||
|
|
this.mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1;
|
||
|
|
this.mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||
|
|
|
||
|
|
// Intersect with floor/room to find drop position
|
||
|
|
const intersects = this.raycaster.intersectObjects(this.selectable, true);
|
||
|
|
|
||
|
|
let position = new THREE.Vector3(0, 0, 0);
|
||
|
|
|
||
|
|
if (intersects.length > 0) {
|
||
|
|
|
||
|
|
position.copy(intersects[0].point);
|
||
|
|
|
||
|
|
} else {
|
||
|
|
|
||
|
|
// If no intersection, drop at default height and camera forward direction
|
||
|
|
position.set(0, 0, 0);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
let geometry;
|
||
|
|
|
||
|
|
switch (primitiveType) {
|
||
|
|
|
||
|
|
case 'cube':
|
||
|
|
|
||
|
|
geometry = new THREE.BoxGeometry(1, 1, 1);
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'sphere':
|
||
|
|
|
||
|
|
geometry = new THREE.SphereGeometry(0.5, 16, 16);
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'cylinder':
|
||
|
|
|
||
|
|
geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 16);
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
|
||
|
|
return;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
const material = new THREE.MeshStandardMaterial({ color: 0x7777ff, metalness: 0.3, roughness: 0.6 });
|
||
|
|
|
||
|
|
const mesh = new THREE.Mesh(geometry, material);
|
||
|
|
|
||
|
|
mesh.userData.isPrimitive = true;
|
||
|
|
|
||
|
|
mesh.userData.type = primitiveType; // or 'sphere', 'cone', etc.
|
||
|
|
|
||
|
|
mesh.position.copy(position);
|
||
|
|
|
||
|
|
mesh.castShadow = true;
|
||
|
|
|
||
|
|
mesh.receiveShadow = true;
|
||
|
|
|
||
|
|
mesh.name = primitiveType;
|
||
|
|
|
||
|
|
|
||
|
|
this.sceneObjects.push(mesh);
|
||
|
|
|
||
|
|
this.scene.add(mesh);
|
||
|
|
|
||
|
|
this.selectable.push(mesh);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
onDropMaterial(event) {
|
||
|
|
event.preventDefault();
|
||
|
|
const materialIndex = event.dataTransfer.getData('materialIndex');
|
||
|
|
if (materialIndex === '') return;
|
||
|
|
const index = parseInt(materialIndex);
|
||
|
|
if (isNaN(index) || !this.materials[index]) return;
|
||
|
|
|
||
|
|
|
||
|
|
const canvas = this.renderer.domElement;
|
||
|
|
const rect = canvas.getBoundingClientRect();
|
||
|
|
|
||
|
|
this.mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1;
|
||
|
|
this.mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1;
|
||
|
|
|
||
|
|
|
||
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||
|
|
const intersects = this.raycaster.intersectObjects(this.selectable, true);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if (intersects.length > 0) {
|
||
|
|
const obj = intersects[0].object;
|
||
|
|
obj.material = this.materials[index].material;
|
||
|
|
this.attachMaterialControls(obj.material);
|
||
|
|
|
||
|
|
this.savePrimitivesToStorage();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
addOrUpdateMaterial() {
|
||
|
|
const name = this.matNameInput.value.trim();
|
||
|
|
const color = this.colorInput.value;
|
||
|
|
if (!name) return;
|
||
|
|
|
||
|
|
const hexColor = new THREE.Color(color);
|
||
|
|
let mat;
|
||
|
|
|
||
|
|
if (this.selectedMaterialIndex >= 0) {
|
||
|
|
mat = this.materials[this.selectedMaterialIndex].material;
|
||
|
|
mat.color = hexColor;
|
||
|
|
this.materials[this.selectedMaterialIndex].name = name;
|
||
|
|
} else {
|
||
|
|
mat = new THREE.MeshStandardMaterial({ color: hexColor });
|
||
|
|
this.materials.push({ name, material: mat });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.selectedObject) {
|
||
|
|
this.selectedObject.material = mat;
|
||
|
|
this.attachMaterialControls(mat);
|
||
|
|
}
|
||
|
|
|
||
|
|
this.saveMaterialsToStorage(mat);
|
||
|
|
this.matNameInput.value = '';
|
||
|
|
this.selectedMaterialIndex = -1;
|
||
|
|
this.refreshMaterialList();
|
||
|
|
}
|
||
|
|
applyTexture(mapType, fileOrUrl) {
|
||
|
|
|
||
|
|
const textureLoader = new THREE.TextureLoader();
|
||
|
|
|
||
|
|
if (typeof fileOrUrl === 'string') {
|
||
|
|
// Normal URL (e.g. '/tiles/texture.jpg')
|
||
|
|
textureLoader.load(fileOrUrl, (texture) => {
|
||
|
|
this._applyLoadedTexture(mapType, texture, fileOrUrl);
|
||
|
|
});
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// It's a File - convert to Base64 first
|
||
|
|
const reader = new FileReader();
|
||
|
|
|
||
|
|
reader.onload = (event) => {
|
||
|
|
const base64url = event.target.result;
|
||
|
|
|
||
|
|
textureLoader.load(base64url, (texture) => {
|
||
|
|
this._applyLoadedTexture(mapType, texture, base64url);
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
reader.readAsDataURL(fileOrUrl);
|
||
|
|
}
|
||
|
|
|
||
|
|
_applyLoadedTexture(mapType, texture, url) {
|
||
|
|
|
||
|
|
if (this.selectedMaterialIndex < 0) return;
|
||
|
|
|
||
|
|
const matWrapper = this.materials[this.selectedMaterialIndex];
|
||
|
|
|
||
|
|
matWrapper.material[mapType] = texture;
|
||
|
|
|
||
|
|
matWrapper.material.needsUpdate = true;
|
||
|
|
|
||
|
|
matWrapper[mapType + 'URL'] = url;
|
||
|
|
|
||
|
|
this.saveMaterialsToStorage(matWrapper.material);
|
||
|
|
|
||
|
|
// If you want to also update UI sliders etc, call a method here if you have one
|
||
|
|
}
|
||
|
|
|
||
|
|
setTextureOnMaterial(mapType, texture, url) {
|
||
|
|
if (this.selectedMaterialIndex < 0) return;
|
||
|
|
|
||
|
|
const matEntry = this.materials[this.selectedMaterialIndex];
|
||
|
|
const mat = matEntry.material;
|
||
|
|
|
||
|
|
mat[mapType] = texture;
|
||
|
|
|
||
|
|
mat.needsUpdate = true;
|
||
|
|
|
||
|
|
// Save URL if texture is from a path (null if local file blob)
|
||
|
|
if (url) {
|
||
|
|
matEntry[mapType + 'URL'] = url;
|
||
|
|
} else {
|
||
|
|
matEntry[mapType + 'URL'] = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.saveMaterialsToStorage(mat);
|
||
|
|
|
||
|
|
// If a mesh is selected, update its material reference
|
||
|
|
if (this.selectedObject) {
|
||
|
|
this.selectedObject.material = mat;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
updateUIFromMaterial(material) {
|
||
|
|
|
||
|
|
this.colorInput.value = '#' + material.color.getHexString();
|
||
|
|
|
||
|
|
this.roughnessInput.value = material.roughness;
|
||
|
|
|
||
|
|
this.metalnessInput.value = material.metalness;
|
||
|
|
|
||
|
|
this.displacementInput.value = material.displacementScale || 0;
|
||
|
|
|
||
|
|
this.opacityInput.value = material.opacity || 1;
|
||
|
|
|
||
|
|
// If needed, add code to update texture preview thumbnails or inputs too
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
attachMaterialControls(mat) {
|
||
|
|
this.updateUIFromMaterial(mat);
|
||
|
|
this.colorInput.oninput = () => {
|
||
|
|
mat.color.set(this.colorInput.value);
|
||
|
|
mat.needsUpdate = true;
|
||
|
|
this.refreshMaterialList();
|
||
|
|
};
|
||
|
|
this.roughnessInput.oninput = () => {
|
||
|
|
mat.roughness = parseFloat(this.roughnessInput.value);
|
||
|
|
mat.needsUpdate = true;
|
||
|
|
};
|
||
|
|
this.metalnessInput.oninput = () => {
|
||
|
|
mat.metalness = parseFloat(this.metalnessInput.value);
|
||
|
|
mat.needsUpdate = true;
|
||
|
|
};
|
||
|
|
this.displacementInput.oninput = () => {
|
||
|
|
mat.displacementScale = parseFloat(this.displacementInput.value);
|
||
|
|
mat.needsUpdate = true;
|
||
|
|
};
|
||
|
|
this.opacityInput.oninput = () => {
|
||
|
|
mat.opacity = parseFloat(this.opacityInput.value);
|
||
|
|
mat.transparent = mat.opacity < 1;
|
||
|
|
mat.needsUpdate = true;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
initCamera() {
|
||
|
|
this.camera.position.set(0, 1.5, 6);
|
||
|
|
this.camera.lookAt(0, 0, 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
initRenderer() {
|
||
|
|
//this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||
|
|
this.camera.updateProjectionMatrix();
|
||
|
|
//this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||
|
|
|
||
|
|
|
||
|
|
this.renderer.shadowMap.enabled = true;
|
||
|
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||
|
|
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||
|
|
this.renderer.toneMappingExposure = 1.3;
|
||
|
|
this.renderer.setClearColor(0x151515);
|
||
|
|
//document.body.appendChild(this.renderer.domElement);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
initLights() {
|
||
|
|
this.pointLight = new THREE.PointLight(0xffddaa, 18, 30, 1.5);
|
||
|
|
this.pointLight.position.set(0, 1.5, 3.5);
|
||
|
|
this.pointLight.castShadow = true;
|
||
|
|
this.pointLight.shadow.mapSize.set(2048, 2048);
|
||
|
|
this.pointLight.shadow.radius = 2;
|
||
|
|
const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.1), new THREE.MeshBasicMaterial({ color: 0xffddaa }));
|
||
|
|
this.pointLight.add(bulb);
|
||
|
|
this.scene.add(this.pointLight);
|
||
|
|
this.scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
||
|
|
this.scene.add(new THREE.HemisphereLight(0xfff0e0, 0x222244, 0.3));
|
||
|
|
}
|
||
|
|
|
||
|
|
initControls() {
|
||
|
|
|
||
|
|
this.controls = new OrbitControls( this.camera, this.renderer.domElement );
|
||
|
|
|
||
|
|
this.controls.enableDamping = true;
|
||
|
|
this.controls.dampingFactor = 0.05;
|
||
|
|
|
||
|
|
this.controls.zoomSpeed = 0.02; // Not too small
|
||
|
|
this.controls.minDistance = 4;
|
||
|
|
this.controls.maxDistance = 10;
|
||
|
|
|
||
|
|
this.controls.enableZoom = true;
|
||
|
|
this.controls.mouseButtons = {
|
||
|
|
LEFT: THREE.MOUSE.ROTATE,
|
||
|
|
MIDDLE: THREE.MOUSE.DOLLY,
|
||
|
|
RIGHT: THREE.MOUSE.PAN
|
||
|
|
};
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
initPostProcessing() {
|
||
|
|
|
||
|
|
this.composer = new EffectComposer(this.renderer);
|
||
|
|
|
||
|
|
const renderPass = new RenderPass(this.scene, this.camera);
|
||
|
|
this.composer.addPass(renderPass);
|
||
|
|
|
||
|
|
this.ssaoPass = new SSAOPass(this.scene, this.camera, window.innerWidth, window.innerHeight);
|
||
|
|
this.ssaoPass.kernelRadius = 6;
|
||
|
|
this.ssaoPass.minDistance = 0.02;
|
||
|
|
this.ssaoPass.maxDistance = 0.15;
|
||
|
|
|
||
|
|
this.composer.addPass(this.ssaoPass);
|
||
|
|
|
||
|
|
this.composer.renderTarget1.depthTexture = new THREE.DepthTexture();
|
||
|
|
this.composer.renderTarget1.depthTexture.type = THREE.UnsignedShortType;
|
||
|
|
|
||
|
|
this.ssaoPass.depthTexture = this.composer.renderTarget1.depthTexture;
|
||
|
|
}
|
||
|
|
|
||
|
|
async loadMaterialFromFolder(name, folderPath) {
|
||
|
|
|
||
|
|
const loader = new THREE.TextureLoader();
|
||
|
|
const mapPaths = {
|
||
|
|
mapURL: `${folderPath}/${name}/${name}_Color.jpg`,
|
||
|
|
normalMapURL: `${folderPath}/${name}/${name}_NormalGL.jpg`,
|
||
|
|
roughnessMapURL: `${folderPath}/${name}/${name}_Roughness.jpg`,
|
||
|
|
displacementMapURL: `${folderPath}/${name}/${name}_Displacement.jpg`,
|
||
|
|
alphaMapURL: `${folderPath}/${name}/${name}_Alpha.jpg`,
|
||
|
|
aoMapURL: `${folderPath}/${name}/${name}_AmbientOcclusion.jpg`
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
console.log(mapPaths);
|
||
|
|
const material = new THREE.MeshStandardMaterial({
|
||
|
|
color: 0xffffff,
|
||
|
|
metalness: 0.2,
|
||
|
|
roughness: 0.6,
|
||
|
|
displacementScale: 0.03
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const [key, url] of Object.entries(mapPaths)) {
|
||
|
|
try {
|
||
|
|
const texture = await new Promise((resolve) => {
|
||
|
|
loader.load(url, resolve, undefined, () => resolve(null));
|
||
|
|
});
|
||
|
|
|
||
|
|
if (texture) {
|
||
|
|
const mapType = key.replace("URL", ""); // mapURL → map
|
||
|
|
material[mapType] = texture;
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.warn("Failed to load texture:", url);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
material.needsUpdate = true;
|
||
|
|
|
||
|
|
this.materials.push({
|
||
|
|
name: name,
|
||
|
|
material: material,
|
||
|
|
...mapPaths
|
||
|
|
});
|
||
|
|
|
||
|
|
this.selectedMaterialIndex = this.materials.length - 1;
|
||
|
|
console.log("material", this.materials);
|
||
|
|
this.refreshMaterialList();
|
||
|
|
|
||
|
|
this.saveMaterialsToStorage(material);
|
||
|
|
}
|
||
|
|
|
||
|
|
initTransformControls() {
|
||
|
|
this.transformControls = new TransformControls(this.camera, this.renderer.domElement);
|
||
|
|
this.transformControls.addEventListener('dragging-changed', (e) => {
|
||
|
|
this.controls.enabled = !e.value;
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
this.transformControls.addEventListener('objectChange', () => {
|
||
|
|
this.savePrimitivesToStorage();
|
||
|
|
});
|
||
|
|
|
||
|
|
this.scene.add(this.transformControls);
|
||
|
|
|
||
|
|
document.getElementById('translateBtn').onclick = () => this.transformControls.setMode('translate');
|
||
|
|
document.getElementById('rotateBtn').onclick = () => this.transformControls.setMode('rotate');
|
||
|
|
document.getElementById('scaleBtn').onclick = () => this.transformControls.setMode('scale');
|
||
|
|
document.getElementById('noneBtn').onclick = () => this.transformControls.detach();
|
||
|
|
}
|
||
|
|
|
||
|
|
addEventListeners() {
|
||
|
|
|
||
|
|
window.addEventListener('resize', this.onResize.bind(this));
|
||
|
|
|
||
|
|
window.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||
|
|
|
||
|
|
this.renderer.domElement.addEventListener('click', this.onClick.bind(this));
|
||
|
|
|
||
|
|
this.renderer.domElement.addEventListener('dragover', (e) => e.preventDefault());
|
||
|
|
|
||
|
|
this.renderer.domElement.addEventListener('drop', this.onDropPrimitive.bind(this));
|
||
|
|
|
||
|
|
window.addEventListener('resize', this.onResize.bind(this));
|
||
|
|
|
||
|
|
window.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||
|
|
|
||
|
|
this.renderer.domElement.addEventListener('click', this.onClick.bind(this));
|
||
|
|
}
|
||
|
|
|
||
|
|
onResize() {
|
||
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||
|
|
this.camera.updateProjectionMatrix();
|
||
|
|
//this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||
|
|
|
||
|
|
const canvas = document.getElementById("threeCanvas");
|
||
|
|
|
||
|
|
const rightPanel = document.getElementById("rightPanel");
|
||
|
|
const pixelRatio = window.devicePixelRatio || 1;
|
||
|
|
|
||
|
|
const width = canvas.clientWidth;
|
||
|
|
const height = canvas.clientHeight;
|
||
|
|
console.log(canvas, rightPanel, width-rightPanel.clientWidth);
|
||
|
|
|
||
|
|
this.renderer.setSize( width-rightPanel.clientWidth, height, false );
|
||
|
|
this.renderer.setPixelRatio( pixelRatio );
|
||
|
|
|
||
|
|
if (this.composer) {
|
||
|
|
this.composer.setSize(width-rightPanel.clientWidth, height);
|
||
|
|
this.ssaoPass.setSize(width-rightPanel.clientWidth, height);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
if (this.composer) this.composer.setSize(window.innerWidth, window.innerHeight);
|
||
|
|
}
|
||
|
|
|
||
|
|
onMouseMove(event) {
|
||
|
|
const canvas = this.renderer.domElement;
|
||
|
|
const rect = canvas.getBoundingClientRect();
|
||
|
|
|
||
|
|
this.mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1;
|
||
|
|
this.mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1;
|
||
|
|
|
||
|
|
this.raycaster.setFromCamera( this.mouse, this.camera );
|
||
|
|
this.pointLight.position.x = this.mouse.x * 3;
|
||
|
|
|
||
|
|
this.pointLight.position.y = this.mouse.y * 1 + 1.5;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
onClick(event) {
|
||
|
|
|
||
|
|
|
||
|
|
const canvas = this.renderer.domElement;
|
||
|
|
const rect = canvas.getBoundingClientRect();
|
||
|
|
|
||
|
|
this.mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1;
|
||
|
|
this.mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1;
|
||
|
|
|
||
|
|
|
||
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||
|
|
const intersects = this.raycaster.intersectObjects(this.selectable, true);
|
||
|
|
if (intersects.length > 0) {
|
||
|
|
this.selectedObject = intersects[0].object;
|
||
|
|
this.transformControls.attach(this.selectedObject);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
addRoom() {
|
||
|
|
const textureLoader = new THREE.TextureLoader();
|
||
|
|
const material = new THREE.MeshPhysicalMaterial({
|
||
|
|
map: textureLoader.load('tiles/Tiles133A_1K-JPG_Color.jpg'),
|
||
|
|
normalMap: textureLoader.load('tiles/Tiles133A_1K-JPG_NormalGL.jpg'),
|
||
|
|
roughnessMap: textureLoader.load('tiles/Tiles133A_1K-JPG_Roughness.jpg'),
|
||
|
|
displacementMap: textureLoader.load('tiles/Tiles133A_1K-JPG_Displacement.jpg'),
|
||
|
|
side: THREE.BackSide,
|
||
|
|
metalness: 0.2,
|
||
|
|
roughness: 0.6,
|
||
|
|
displacementScale: 0.01
|
||
|
|
});
|
||
|
|
const room = new THREE.Mesh(new THREE.BoxGeometry(16, 14, 16), material);
|
||
|
|
room.receiveShadow = true;
|
||
|
|
this.scene.add(room);
|
||
|
|
}
|
||
|
|
|
||
|
|
addBigBox() {
|
||
|
|
const boxMaterial = new THREE.MeshStandardMaterial({
|
||
|
|
color: 'rgb(34,139,34)',
|
||
|
|
metalness: 0.2,
|
||
|
|
roughness: 0.7
|
||
|
|
});
|
||
|
|
const box = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), boxMaterial);
|
||
|
|
box.position.set(1.5, 0, 0);
|
||
|
|
box.castShadow = true;
|
||
|
|
box.receiveShadow = true;
|
||
|
|
this.selectable.push(box);
|
||
|
|
this.scene.add(box);
|
||
|
|
}
|
||
|
|
|
||
|
|
animate() {
|
||
|
|
requestAnimationFrame(this.animate.bind(this));
|
||
|
|
this.controls.update();
|
||
|
|
if (this.composer) {
|
||
|
|
this.composer.render();
|
||
|
|
} else {
|
||
|
|
this.renderer.render(this.scene, this.camera);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.previewRenderer && this.previewScene && this.previewCamera) {
|
||
|
|
this.previewRenderer.render(this.previewScene, this.previewCamera);
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
new TextureEditorApp();
|