文章目录
序
要实现一个类似于数字孪生的场景 可以在线、新增、删除模型 、以及编辑模型的颜色、长宽高
然后还要实现 编辑完后 保存为json数据 记录模型数据 既可以导入也可以导出
一、1.0.0版本
1.新增
先拿建议的立方体来代替模型
点击新增按钮就新增一个立方体
2.编辑
点击编辑按钮可以修改坐标 长宽高 颜色等等信息
3.导出
点击导出按钮 可以导出为json数据格式
4.导入
选择导入刚才的json文件
有一个bug 就是导入后颜色丢失了 点击模型 信息面板的颜色显示正常 渲染颜色丢失
源码
<template>
<div id="app" @click="onAppClick">
<div id="info">
<button @click.stop="addBuilding">新增</button>
<button @click.stop="showEditor">编辑</button>
<button @click.stop="exportModelData">导出</button>
<input type="file" @change="importModelData" ref="fileInput" />
</div>
<div id="editor" v-if="editorVisible" @click.stop>
<h3>Edit Building</h3>
<label for="color">Color:</label>
<input type="color" id="color" v-model="selectedObjectProps.color" /><br />
<label for="posX">Position X:</label>
<input
type="number"
id="posX"
v-model="selectedObjectProps.posX"
step="0.1"
/><br />
<label for="posY">Position Y:</label>
<input
type="number"
id="posY"
v-model="selectedObjectProps.posY"
step="0.1"
/><br />
<label for="posZ">Position Z:</label>
<input
type="number"
id="posZ"
v-model="selectedObjectProps.posZ"
step="0.1"
/><br />
<label for="scaleX">Scale X:</label>
<input
type="number"
id="scaleX"
v-model="selectedObjectProps.scaleX"
step="0.1"
/><br />
<label for="scaleY">Scale Y:</label>
<input
type="number"
id="scaleY"
v-model="selectedObjectProps.scaleY"
step="0.1"
/><br />
<label for="scaleZ">Scale Z:</label>
<input
type="number"
id="scaleZ"
v-model="selectedObjectProps.scaleZ"
step="0.1"
/><br />
<label for="rotX">Rotation X:</label>
<input
type="number"
id="rotX"
v-model="selectedObjectProps.rotX"
step="0.1"
/><br />
<label for="rotY">Rotation Y:</label>
<input
type="number"
id="rotY"
v-model="selectedObjectProps.rotY"
step="0.1"
/><br />
<label for="rotZ">Rotation Z:</label>
<input
type="number"
id="rotZ"
v-model="selectedObjectProps.rotZ"
step="0.1"
/><br />
<button @click="applyEdit">保存</button>
<button @click="deleteBuilding">删除</button>
</div>
<div ref="canvasContainer" style="width: 100vw; height: 100vh"></div>
</div>
</template>
<script>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
export default {
data() {
return {
editorVisible: false,
selectedObject: null,
selectedObjectProps: {
color: "#00ff00",
posX: 0,
posY: 0,
posZ: 0,
scaleX: 1,
scaleY: 1,
scaleZ: 1,
rotX: 0,
rotY: 0,
rotZ: 0,
},
raycaster: null,
};
},
mounted() {
this.init();
this.animate();
window.addEventListener("resize", this.onWindowResize, false);
this.loadModelData(); // Load saved model data on page load
},
methods: {
init() {
console.log("Initializing Three.js");
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xcccccc);
this.camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.set(0, 10, 20);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.$refs.canvasContainer.appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 7.5);
this.scene.add(light);
this.raycaster = new THREE.Raycaster();
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
this.cube = new THREE.Mesh(geometry, material);
this.scene.add(this.cube);
},
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
},
onAppClick(event) {
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
this.raycaster.setFromCamera(mouse, this.camera);
const intersects = this.raycaster.intersectObjects(this.scene.children, true);
if (intersects.length > 0) {
this.selectedObject = intersects[0].object;
console.log("Object selected:", this.selectedObject);
this.showEditor();
}
},
addBuilding() {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const building = new THREE.Mesh(geometry, material);
building.position.set(Math.random() * 10 - 5, 0.5, Math.random() * 10 - 5);
this.scene.add(building);
},
showEditor() {
if (this.selectedObject) {
this.editorVisible = true;
this.updateEditor(this.selectedObject);
}
},
updateEditor(object) {
this.selectedObjectProps.color = `#${object.material.color.getHexString()}`;
this.selectedObjectProps.posX = object.position.x;
this.selectedObjectProps.posY = object.position.y;
this.selectedObjectProps.posZ = object.position.z;
this.selectedObjectProps.scaleX = object.scale.x;
this.selectedObjectProps.scaleY = object.scale.y;
this.selectedObjectProps.scaleZ = object.scale.z;
this.selectedObjectProps.rotX = object.rotation.x;
this.selectedObjectProps.rotY = object.rotation.y;
this.selectedObjectProps.rotZ = object.rotation.z;
},
applyEdit() {
if (this.selectedObject) {
const color = this.selectedObjectProps.color;
this.selectedObject.material.color.set(color);
this.selectedObject.position.set(
parseFloat(this.selectedObjectProps.posX),
parseFloat(this.selectedObjectProps.posY),
parseFloat(this.selectedObjectProps.posZ)
);
this.selectedObject.scale.set(
parseFloat(this.selectedObjectProps.scaleX),
parseFloat(this.selectedObjectProps.scaleY),
parseFloat(this.selectedObjectProps.scaleZ)
);
this.selectedObject.rotation.set(
parseFloat(this.selectedObjectProps.rotX),
parseFloat(this.selectedObjectProps.rotY),
parseFloat(this.selectedObjectProps.rotZ)
);
}
},
deleteBuilding() {
if (this.selectedObject) {
this.scene.remove(this.selectedObject);
this.selectedObject = null;
this.editorVisible = false;
}
},
animate() {
requestAnimationFrame(this.animate);
this.renderer.render(this.scene, this.camera);
this.controls.update();
},
exportModelData() {
const modelData = {
objects: this.scene.children
.filter((obj) => obj instanceof THREE.Mesh) // 过滤出是 Mesh 对象的物体
.map((obj) => ({
position: obj.position.toArray(),
scale: obj.scale.toArray(),
rotation: obj.rotation.toArray(),
color: `#${obj.material.color.getHexString()}`,
})),
};
const jsonData = JSON.stringify(modelData);
const blob = new Blob([jsonData], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = "model_data.json";
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
},
importModelData(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
console.log("Imported data:", data); // 输出导入的完整数据,确保格式和内容正确
this.clearScene();
data.objects.forEach((objData, index) => {
const geometry = new THREE.BoxGeometry();
// 设置默认颜色为红色
const color = new THREE.Color(0xff0000); // 红色
// 如果数据中有颜色字段并且是合法的颜色值,则使用数据中的颜色
if (objData.color && typeof objData.color === "string") {
try {
color.set(objData.color);
} catch (error) {
console.error(`Error parsing color for object ${index}:`, error);
}
} else {
console.warn(`Invalid color value for object ${index}:`, objData.color);
}
const material = new THREE.MeshStandardMaterial({
color: color,
metalness: 0.5, // 示例中的金属度设置为0.5,可以根据需求调整
roughness: 0.8, // 示例中的粗糙度设置为0.8,可以根据需求调整
});
const object = new THREE.Mesh(geometry, material);
object.position.fromArray(objData.position);
object.scale.fromArray(objData.scale);
object.rotation.fromArray(objData.rotation);
this.scene.add(object);
});
} catch (error) {
console.error("Error importing model data:", error);
}
};
reader.readAsText(file);
}
},
clearScene() {
while (this.scene.children.length > 0) {
this.scene.remove(this.scene.children[0]);
}
},
saveModelData() {
const modelData = {
objects: this.scene.children.map((obj) => ({
position: obj.position.toArray(),
scale: obj.scale.toArray(),
rotation: obj.rotation.toArray(),
color: `#${obj.material.color.getHexString()}`,
})),
};
localStorage.setItem("modelData", JSON.stringify(modelData));
},
loadModelData() {
const savedData = localStorage.getItem("modelData");
if (savedData) {
try {
const data = JSON.parse(savedData);
this.clearScene();
data.objects.forEach((objData) => {
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({
color: parseInt(objData.color.replace("#", "0x"), 16),
});
const object = new THREE.Mesh(geometry, material);
object.position.fromArray(objData.position);
object.scale.fromArray(objData.scale);
object.rotation.fromArray(objData.rotation);
this.scene.add(object);
});
} catch (error) {
console.error("Error loading model data from localStorage:", error);
}
}
},
},
};
</script>
<style>
body {
margin: 0;
overflow: hidden;
}
canvas {
display: block;
}
#info {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
}
#editor {
position: absolute;
top: 100px;
left: 10px;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
}
</style>
二、2.0.0版本
1. 修复模型垂直方向放置时 模型会重合
4. 修复了导出导入功能 现在是1:1导出导入
5. 新增一个地面 视角看不到地下 设置了禁止编辑地面 地面设置为圆形
6. 新增功能 可选择基本圆形 方形 圆柱形等模型以及可放置自己的模型文件
7. 优化面板样式
<template>
<div id="app" @click="onAppClick">
<div id="info">
<button @click.stop="toggleBuildingMode">
{{ buildingMode ? "关闭建造模式" : "开启建造模式" }}
</button>
<button @click.stop="showEditor">编辑所选模型</button>
<button @click.stop="exportModelData">导出模型数据</button>
<input type="file" @change="importModelData" ref="fileInput" />
<input type="file" @change="importCustomModel" ref="customModelInput" />
<label for="modelType">模型类型:</label>
<select v-model="selectedModelType">
<option value="box">立方体</option>
<option value="sphere">球体</option>
<option value="cylinder">圆柱体</option>
<option value="custom">自定义模型</option>
</select>
</div>
<div id="editor" v-if="editorVisible" @click.stop>
<h3>编辑模型</h3>
<div class="form-group">
<label for="color">颜色:</label>
<input type="color" id="color" v-model="selectedObjectProps.color" /><br />
</div>
<div class="form-group">
<label for="posX">位置 X:</label>
<input type="number" id="posX" v-model="selectedObjectProps.posX" step="0.1" /><br />
</div>
<div class="form-group">
<label for="posY">位置 Y:</label>
<input type="number" id="posY" v-model="selectedObjectProps.posY" step="0.1" /><br />
</div>
<div class="form-group">
<label for="posZ">位置 Z:</label>
<input type="number" id="posZ" v-model="selectedObjectProps.posZ" step="0.1" /><br />
</div>
<div class="form-group">
<label for="scaleX">缩放 X:</label>
<input type="number" id="scaleX" v-model="selectedObjectProps.scaleX" step="0.1" /><br />
</div>
<div class="form-group">
<label for="scaleY">缩放 Y:</label>
<input type="number" id="scaleY" v-model="selectedObjectProps.scaleY" step="0.1" /><br />
</div>
<div class="form-group">
<label for="scaleZ">缩放 Z:</label>
<input type="number" id="scaleZ" v-model="selectedObjectProps.scaleZ" step="0.1" /><br />
</div>
<div class="form-group">
<label for="rotX">旋转 X:</label>
<input type="number" id="rotX" v-model="selectedObjectProps.rotX" step="0.1" /><br />
</div>
<div class="form-group">
<label for="rotY">旋转 Y:</label>
<input type="number" id="rotY" v-model="selectedObjectProps.rotY" step="0.1" /><br />
</div>
<div class="form-group">
<label for="rotZ">旋转 Z:</label>
<input type="number" id="rotZ" v-model="selectedObjectProps.rotZ" step="0.1" /><br />
</div>
<button @click="applyEdit">应用</button>
<button @click="deleteBuilding">删除</button>
</div>
<div ref="canvasContainer" style="width: 100vw; height: 100vh"></div>
</div>
</template>
<script>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
export default {
data() {
return {
editorVisible: false,
selectedObject: null,
selectedObjectProps: {
color: "#000",
posX: 0,
posY: 0,
posZ: 0,
scaleX: 1,
scaleY: 1,
scaleZ: 1,
rotX: 0,
rotY: 0,
rotZ: 0,
},
raycaster: null,
buildingMode: false,
selectedModelType: "box",
customModel: null,
};
},
mounted() {
this.init();
this.animate();
window.addEventListener("resize", this.onWindowResize, false);
},
methods: {
animate() {
requestAnimationFrame(this.animate);
this.renderer.render(this.scene, this.camera);
this.controls.update();
},
init() {
console.log("Initializing Three.js");
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color('0xcccccc');
this.camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.set(0, 10, 20);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.$refs.canvasContainer.appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.minDistance = 10;
this.controls.maxDistance = 50;
this.controls.maxPolarAngle = Math.PI / 2;
const planeGeometry = new THREE.CircleGeometry(100, 32);
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x999999 });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.userData.isGround = true;
this.scene.add(plane);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 7.5);
this.scene.add(light);
this.raycaster = new THREE.Raycaster();
},
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
},
onAppClick(event) {
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
this.raycaster.setFromCamera(mouse, this.camera);
const intersects = this.raycaster.intersectObjects(this.scene.children, true);
if (this.buildingMode && intersects.length > 0) {
const intersect = intersects[0];
const point = intersect.point;
if (intersect.object.userData.isGround) {
if (this.isOverlapping(point.x, point.z)) {
this.stackBuilding(point.x, point.z);
} else {
this.addBuilding(point.x, 0, point.z);
}
} else {
const stackHeight = intersect.object.position.y + intersect.object.scale.y;
this.addBuilding(intersect.object.position.x, stackHeight, intersect.object.position.z);
}
} else if (intersects.length > 0) {
this.selectedObject = intersects[0].object;
console.log("Object selected:", this.selectedObject);
this.showEditor();
}
},
isOverlapping(x, z) {
const threshold = 1;
for (let obj of this.scene.children) {
if (
Math.abs(obj.position.x - x) < threshold &&
Math.abs(obj.position.z - z) < threshold &&
!obj.userData.isGround
) {
return true;
}
}
return false;
},
stackBuilding(x, z) {
let maxY = 0;
this.scene.children.forEach((obj) => {
if (
Math.abs(obj.position.x - x) < 1 &&
Math.abs(obj.position.z - z) < 1 &&
!obj.userData.isGround &&
obj.position.y + obj.scale.y > maxY
) {
maxY = obj.position.y + obj.scale.y;
}
});
this.addBuilding(x, maxY, z);
},
addBuilding(x, y, z) {
let geometry;
switch (this.selectedModelType) {
case "sphere":
geometry = new THREE.SphereGeometry(0.5, 32, 32);
break;
case "cylinder":
geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32);
break;
case "custom":
if (this.customModel) {
this.loadCustomModel(x, y, z);
return;
}
break;
case "box":
default:
geometry = new THREE.BoxGeometry(1, 1, 1);
break;
}
if (geometry) {
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const building = new THREE.Mesh(geometry, material);
building.position.set(x, y, z);
this.scene.add(building);
}
},
loadCustomModel(x, y, z) {
const loader = new GLTFLoader();
loader.load(
this.customModel,
(gltf) => {
const object = gltf.scene;
object.position.set(x, y, z);
this.scene.add(object);
},
undefined,
(error) => {
console.error("An error happened while loading the custom model", error);
}
);
},
importCustomModel(event) {
const file = event.target.files[0];
this.customModel = URL.createObjectURL(file);
},
showEditor() {
if (this.selectedObject) {
this.selectedObjectProps.color = "#" + this.selectedObject.material.color.getHexString();
this.selectedObjectProps.posX = this.selectedObject.position.x;
this.selectedObjectProps.posY = this.selectedObject.position.y;
this.selectedObjectProps.posZ = this.selectedObject.position.z;
this.selectedObjectProps.scaleX = this.selectedObject.scale.x;
this.selectedObjectProps.scaleY = this.selectedObject.scale.y;
this.selectedObjectProps.scaleZ = this.selectedObject.scale.z;
this.selectedObjectProps.rotX = this.selectedObject.rotation.x;
this.selectedObjectProps.rotY = this.selectedObject.rotation.y;
this.selectedObjectProps.rotZ = this.selectedObject.rotation.z;
}
this.editorVisible = true;
},
applyEdit() {
if (this.selectedObject) {
this.selectedObject.material.color.set(this.selectedObjectProps.color);
this.selectedObject.position.set(
this.selectedObjectProps.posX,
this.selectedObjectProps.posY,
this.selectedObjectProps.posZ
);
this.selectedObject.scale.set(
this.selectedObjectProps.scaleX,
this.selectedObjectProps.scaleY,
this.selectedObjectProps.scaleZ
);
this.selectedObject.rotation.set(
this.selectedObjectProps.rotX,
this.selectedObjectProps.rotY,
this.selectedObjectProps.rotZ
);
}
this.editorVisible = false;
},
deleteBuilding() {
if (this.selectedObject) {
this.scene.remove(this.selectedObject);
this.selectedObject.geometry.dispose();
this.selectedObject.material.dispose();
this.selectedObject = null;
this.editorVisible = false;
}
},
toggleBuildingMode() {
this.buildingMode = !this.buildingMode;
},
exportModelData() {
const modelData = this.scene.children
.filter((obj) => obj.type === "Mesh" && !obj.userData.isGround)
.map((obj) => ({
type: obj.geometry.type,
position: obj.position,
rotation: obj.rotation,
scale: obj.scale,
color: obj.material.color.getHex(),
}));
const blob = new Blob([JSON.stringify(modelData)], { type: "application/json" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "modelData.json";
link.click();
},
importModelData(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
const modelData = JSON.parse(e.target.result);
this.loadModelData(modelData);
};
reader.readAsText(file);
},
loadModelData(modelData = null) {
if (!modelData) {
return;
}
modelData.forEach((data) => {
let geometry;
switch (data.type) {
case "SphereGeometry":
geometry = new THREE.SphereGeometry(0.5, 32, 32);
break;
case "CylinderGeometry":
geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32);
break;
case "BoxGeometry":
default:
geometry = new THREE.BoxGeometry(1, 1, 1);
break;
}
const material = new THREE.MeshStandardMaterial({ color: data.color });
const object = new THREE.Mesh(geometry, material);
object.position.copy(data.position);
object.rotation.copy(data.rotation);
object.scale.copy(data.scale);
this.scene.add(object);
});
},
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialias;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#info {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 5px;
}
#editor {
position: absolute;
top: 50px;
right: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 5px;
z-index: 1000;
width: 200px;
}
#editor .form-group {
margin-bottom: 10px;
}
#editor label {
display: block;
margin-bottom: 5px;
}
#editor input {
width: 100%;
}
</style>
二、2.0.1版本
1. 修复了删除模型无效
2. 修复了设置地面网格后删除自定义模型无效
3. 加入简易城市json模型
<template>
<div id="app" @click="onAppClick">
<div id="info">
<button @click.stop="toggleBuildingMode">
{{ buildingMode ? "关闭建造模式" : "开启建造模式" }}
</button>
<button @click.stop="showEditor">编辑所选模型</button>
<button @click.stop="exportModelData">导出模型数据</button>
<input type="file" @change="importModelData" ref="fileInput" />
<input type="file" @change="importCustomModel" ref="customModelInput" />
<label for="modelType">模型类型:</label>
<select v-model="selectedModelType">
<option value="box">立方体</option>
<option value="sphere">球体</option>
<option value="cylinder">圆柱体</option>
<option value="custom">自定义模型</option>
<option value="city">城市模型</option>
</select>
</div>
<div id="editor" v-if="editorVisible" @click.stop>
<h3>编辑模型</h3>
<div class="form-group">
<label for="color">颜色:</label>
<input type="color" id="color" v-model="selectedObjectProps.color" /><br />
</div>
<div class="form-group">
<label for="posX">位置 X:</label>
<input
type="number"
id="posX"
v-model="selectedObjectProps.posX"
step="0.1"
/><br />
</div>
<div class="form-group">
<label for="posY">位置 Y:</label>
<input
type="number"
id="posY"
v-model="selectedObjectProps.posY"
step="0.1"
/><br />
</div>
<div class="form-group">
<label for="posZ">位置 Z:</label>
<input
type="number"
id="posZ"
v-model="selectedObjectProps.posZ"
step="0.1"
/><br />
</div>
<div class="form-group">
<label for="scaleX">缩放 X:</label>
<input
type="number"
id="scaleX"
v-model="selectedObjectProps.scaleX"
step="0.1"
/><br />
</div>
<div class="form-group">
<label for="scaleY">缩放 Y:</label>
<input
type="number"
id="scaleY"
v-model="selectedObjectProps.scaleY"
step="0.1"
/><br />
</div>
<div class="form-group">
<label for="scaleZ">缩放 Z:</label>
<input
type="number"
id="scaleZ"
v-model="selectedObjectProps.scaleZ"
step="0.1"
/><br />
</div>
<div class="form-group">
<label for="rotX">旋转 X:</label>
<input
type="number"
id="rotX"
v-model="selectedObjectProps.rotX"
step="0.1"
/><br />
</div>
<div class="form-group">
<label for="rotY">旋转 Y:</label>
<input
type="number"
id="rotY"
v-model="selectedObjectProps.rotY"
step="0.1"
/><br />
</div>
<div class="form-group">
<label for="rotZ">旋转 Z:</label>
<input
type="number"
id="rotZ"
v-model="selectedObjectProps.rotZ"
step="0.1"
/><br />
</div>
<button @click="applyEdit">应用</button>
<button @click="deleteBuilding">删除</button>
</div>
<div ref="canvasContainer" style="width: 100vw; height: 100vh"></div>
</div>
</template>
<script>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
export default {
data() {
return {
editorVisible: false,
selectedObject: null,
selectedObjectProps: {
color: "#000",
posX: 0,
posY: 0,
posZ: 0,
scaleX: 1,
scaleY: 1,
scaleZ: 1,
rotX: 0,
rotY: 0,
rotZ: 0,
},
raycaster: null,
buildingMode: false,
selectedModelType: "box",
customModel: null,
};
},
mounted() {
this.init();
this.animate();
window.addEventListener("resize", this.onWindowResize, false);
},
methods: {
animate() {
requestAnimationFrame(this.animate);
this.renderer.render(this.scene, this.camera);
this.controls.update();
},
init() {
console.log("Initializing Three.js");
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xcccccc);
this.camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.set(0, 10, 20);
// 创建渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.$refs.canvasContainer.appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.minDistance = 10;
this.controls.maxDistance = 50;
this.controls.maxPolarAngle = Math.PI / 2;
// 创建平面地面
const planeSize = 1000;
const divisions = 100;
const planeGeometry = new THREE.PlaneGeometry(
planeSize,
planeSize,
divisions,
divisions
);
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.position.y = -0.5;
plane.userData.isGround = true;
this.scene.add(plane);
// 添加网格线效果
const grid = new THREE.GridHelper(planeSize, divisions, 0xffffff, 0xffffff);
grid.material.opacity = 0.2;
grid.material.transparent = true;
this.scene.add(grid);
// 添加光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 7.5);
this.scene.add(light);
// 初始化射线投射器
this.raycaster = new THREE.Raycaster();
// 监听窗口变化
window.addEventListener("resize", this.onWindowResize, false);
},
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
},
onAppClick(event) {
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
this.raycaster.setFromCamera(mouse, this.camera);
console.log(this.scene.children);
// 过滤掉不需要交互的对象
const interactiveObjects = this.scene.children.filter((obj) => {
return obj.userData.isGround || obj.type === "Mesh" || obj.type === "Group"; // 这里可以根据实际情况添加过滤条件
});
const intersects = this.raycaster.intersectObjects(interactiveObjects, true);
if (this.buildingMode && intersects.length > 0) {
const intersect = intersects[0];
const point = intersect.point;
if (intersect.object.userData.isGround) {
if (this.isOverlapping(point.x, point.z)) {
this.stackBuilding(point.x, point.z);
console.log("Stacking building at", point.x, point.z);
} else {
this.addBuilding(point.x, 0, point.z);
console.log("Adding building at", point.x, 0, point.z);
}
} else {
const stackHeight = intersect.object.position.y + intersect.object.scale.y;
this.addBuilding(
intersect.object.position.x,
stackHeight,
intersect.object.position.z
);
console.log(
"Adding building on top of another object at",
intersect.object.position.x,
stackHeight,
intersect.object.position.z
);
}
} else if (intersects.length > 0) {
this.selectedObject = intersects[0].object;
console.log("Object selected:", this.selectedObject);
this.showEditor();
} else {
console.log("Clicked on empty space");
}
},
isOverlapping(x, z) {
const threshold = 1;
for (let obj of this.scene.children) {
if (
Math.abs(obj.position.x - x) < threshold &&
Math.abs(obj.position.z - z) < threshold &&
!obj.userData.isGround
) {
return true;
}
}
return false;
},
stackBuilding(x, z) {
let maxY = 0;
this.scene.children.forEach((obj) => {
if (
Math.abs(obj.position.x - x) < 1 &&
Math.abs(obj.position.z - z) < 1 &&
!obj.userData.isGround &&
obj.position.y + obj.scale.y > maxY
) {
maxY = obj.position.y + obj.scale.y;
}
});
this.addBuilding(x, maxY, z);
},
addBuilding(x, y, z) {
let geometry;
switch (this.selectedModelType) {
case "sphere":
geometry = new THREE.SphereGeometry(0.5, 32, 32);
break;
case "cylinder":
geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32);
break;
case "custom":
if (this.customModel) {
this.loadCustomModel(x, y, z);
return;
}
break;
case "city":
this.loadCityModel(x, y, z);
return;
case "box":
default:
geometry = new THREE.BoxGeometry(1, 1, 1);
break;
}
if (geometry) {
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const building = new THREE.Mesh(geometry, material);
building.position.set(x, y, z);
this.scene.add(building);
}
},
// 下
// 扩展加载和放置摄像头模型的方法
addCamera(x, y, z) {
const cameraGeometry = new THREE.BoxGeometry(0.2, 0.2, 0.1); // 摄像头的尺寸
const cameraMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 }); // 红色材质表示摄像头
const camera = new THREE.Mesh(cameraGeometry, cameraMaterial);
camera.position.set(x, y, z);
this.scene.add(camera);
},
// 创建摄像头方法
createCamera(cameraData, x, y, z) {
cameraData?.forEach((data) => {
const position = new THREE.Vector3(
data.position.x + x,
data.position.y + y,
data.position.z + z
);
this.addCamera(position.x, position.y, position.z);
});
},
// 修改加载城市模型方法,包含摄像头的加载
parseCityData(cityData) {
const buildingGroup = new THREE.Group();
buildingGroup.name = cityData.buildingName;
cityData.floors.forEach((floor) => {
const floorGroup = new THREE.Group();
floorGroup.position.set(floor.position.x, floor.position.y, floor.position.z);
floor.rooms.forEach((room) => {
const roomGroup = new THREE.Group();
roomGroup.position.set(room.position.x, room.position.y, room.position.z);
room.objects.forEach((object) => {
let mesh;
const material = new THREE.MeshStandardMaterial({ color: object.color });
switch (object.type) {
case "floor":
const floorGeometry = new THREE.BoxGeometry(
object.width,
object.height,
object.depth
);
mesh = new THREE.Mesh(floorGeometry, material);
mesh.position.set(
object.position.x,
object.position.y,
object.position.z
);
roomGroup.add(mesh);
break;
case "wall":
const wallGeometry = new THREE.BoxGeometry(
object.width,
object.height,
object.depth
);
mesh = new THREE.Mesh(wallGeometry, material);
mesh.position.set(
object.position.x,
object.position.y,
object.position.z
);
roomGroup.add(mesh);
break;
}
});
// 处理门和窗户的布尔运算
let wallMeshes = roomGroup.children.filter(
(obj) => obj.material && obj.material.color.getHex() === 0xffffff
);
room.objects.forEach((object) => {
let holeMesh;
switch (object.type) {
case "door":
const doorGeometry = new THREE.BoxGeometry(1, 2, 0.1);
holeMesh = new THREE.Mesh(doorGeometry);
holeMesh.position.set(
object.position.x,
object.position.y + 1,
object.position.z
);
wallMeshes.forEach((wallMesh) => {
let csgWall = THREE.CSG.fromMesh(wallMesh);
let csgHole = THREE.CSG.fromMesh(holeMesh);
let csgResult = csgWall.subtract(csgHole);
let newWallMesh = THREE.CSG.toMesh(csgResult, wallMesh.matrix);
roomGroup.remove(wallMesh);
roomGroup.add(newWallMesh);
});
const doorMaterial = new THREE.MeshStandardMaterial({
color: object.color,
});
const doorMesh = new THREE.Mesh(doorGeometry, doorMaterial);
doorMesh.position.set(
object.position.x,
object.position.y + 1,
object.position.z
);
roomGroup.add(doorMesh);
break;
case "window":
const windowGeometry = new THREE.BoxGeometry(1.5, 1.5, 0.1);
holeMesh = new THREE.Mesh(windowGeometry);
holeMesh.position.set(
object.position.x,
object.position.y + 1.5,
object.position.z
);
wallMeshes.forEach((wallMesh) => {
let csgWall = THREE.CSG.fromMesh(wallMesh);
let csgHole = THREE.CSG.fromMesh(holeMesh);
let csgResult = csgWall.subtract(csgHole);
let newWallMesh = THREE.CSG.toMesh(csgResult, wallMesh.matrix);
roomGroup.remove(wallMesh);
roomGroup.add(newWallMesh);
});
const windowMaterial = new THREE.MeshStandardMaterial({
color: object.color,
});
const windowMesh = new THREE.Mesh(windowGeometry, windowMaterial);
windowMesh.position.set(
object.position.x,
object.position.y + 1.5,
object.position.z
);
roomGroup.add(windowMesh);
break;
case "desk":
const deskGeometry = new THREE.BoxGeometry(2, 1, 1);
const deskMesh = new THREE.Mesh(deskGeometry, material);
deskMesh.position.set(
object.position.x,
object.position.y + 0.5,
object.position.z
);
roomGroup.add(deskMesh);
break;
case "chair":
const chairGeometry = new THREE.BoxGeometry(1, 1, 1);
const chairMesh = new THREE.Mesh(chairGeometry, material);
chairMesh.position.set(
object.position.x,
object.position.y + 0.5,
object.position.z
);
roomGroup.add(chairMesh);
break;
case "computer":
const computerGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const computerMesh = new THREE.Mesh(computerGeometry, material);
computerMesh.position.set(
object.position.x,
object.position.y + 0.25,
object.position.z
);
roomGroup.add(computerMesh);
break;
case "camera":
const cameraGeometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const cameraMesh = new THREE.Mesh(cameraGeometry, material);
cameraMesh.position.set(
object.position.x,
object.position.y + 0.1,
object.position.z
);
roomGroup.add(cameraMesh);
break;
}
});
floorGroup.add(roomGroup);
});
buildingGroup.add(floorGroup);
});
this.scene.add(buildingGroup);
},
loadCityModel(x, y, z) {
const cityModelUrl = "./cityModel.json";
fetch(cityModelUrl)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
this.createBuilding(data, x, y, z);
this.createCamera(data.cameras, x, y, z); // 加载摄像头模型
})
.catch((error) => {
console.error("Failed to load city model:", error);
alert(`Failed to load city model: ${error.message}`);
});
},
createBuilding(buildingData, x, y, z) {
buildingData.floors.forEach((floor) => {
floor.rooms.forEach((room) => {
room.objects.forEach((objectData) => {
let geometry, material;
switch (objectData.type) {
case "floor":
geometry = new THREE.BoxGeometry(
objectData.width,
objectData.height,
objectData.depth
);
material = new THREE.MeshStandardMaterial({ color: objectData.color });
break;
case "wall":
geometry = new THREE.BoxGeometry(
objectData.width,
objectData.height,
objectData.depth
);
material = new THREE.MeshStandardMaterial({ color: objectData.color });
break;
case "door":
geometry = new THREE.BoxGeometry(2, 4, 0.1); // 假设门尺寸为2x4
material = new THREE.MeshStandardMaterial({ color: objectData.color });
// 计算门的位置,需要考虑房间和楼层的偏移
const doorPosition = new THREE.Vector3(
objectData.position.x + room.position.x + floor.position.x + x,
objectData.position.y + room.position.y + floor.position.y + y,
objectData.position.z + room.position.z + floor.position.z + z
);
this.addDoor(geometry, material, doorPosition);
break;
case "window":
geometry = new THREE.BoxGeometry(2, 2, 0.1); // 假设窗户尺寸为2x2
material = new THREE.MeshStandardMaterial({
color: objectData.color,
transparent: true,
opacity: 0.5,
});
// 计算窗户的位置,需要考虑房间和楼层的偏移
const windowPosition = new THREE.Vector3(
objectData.position.x + room.position.x + floor.position.x + x,
objectData.position.y + room.position.y + floor.position.y + y,
objectData.position.z + room.position.z + floor.position.z + z
);
this.addWindow(geometry, material, windowPosition);
break;
case "desk":
geometry = new THREE.BoxGeometry(1, 0.5, 0.5); // 假设桌子尺寸为1x0.5x0.5
material = new THREE.MeshStandardMaterial({ color: objectData.color });
break;
case "chair":
geometry = new THREE.BoxGeometry(0.5, 1, 0.5); // 假设椅子尺寸为0.5x1x0.5
material = new THREE.MeshStandardMaterial({ color: objectData.color });
break;
case "computer":
geometry = new THREE.BoxGeometry(0.3, 0.3, 0.3); // 假设电脑尺寸为0.3x0.3x0.3
material = new THREE.MeshStandardMaterial({ color: objectData.color });
break;
case "camera":
const cameraGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.5, 32);
const cameraMaterial = new THREE.MeshStandardMaterial({
color: objectData.color,
});
const cameraMesh = new THREE.Mesh(cameraGeometry, cameraMaterial);
cameraMesh.position.set(
objectData.position.x +
room.position.x +
floor.position.x +
buildingData.position.x +
x,
objectData.position.y +
room.position.y +
floor.position.y +
buildingData.position.y +
y,
objectData.position.z +
room.position.z +
floor.position.z +
buildingData.position.z +
z
);
this.scene.add(cameraMesh);
break;
default:
throw new Error(`Unsupported object type: ${objectData.type}`);
}
if (
geometry &&
material &&
objectData.type !== "door" &&
objectData.type !== "window"
) {
const object = new THREE.Mesh(geometry, material);
object.position.set(
objectData.position.x +
room.position.x +
floor.position.x +
buildingData.position.x +
x,
objectData.position.y +
room.position.y +
floor.position.y +
buildingData.position.y +
y,
objectData.position.z +
room.position.z +
floor.position.z +
buildingData.position.z +
z
);
this.scene.add(object);
}
});
});
});
},
addDoor(geometry, material, position) {
// 调整门的位置,确保门中心在所需位置
position.y += geometry.parameters.height / 2; // 将门的中心移到地板上
const door = new THREE.Mesh(geometry, material);
door.position.copy(position);
this.scene.add(door);
},
addWindow(geometry, material, position) {
// 创建透明材质
material.transparent = true;
material.opacity = 0.5; // 设置透明度,0为完全透明,1为完全不透明
const windowMesh = new THREE.Mesh(geometry, material);
windowMesh.position.copy(position);
this.scene.add(windowMesh);
},
// 上
loadCustomModel(x, y, z) {
const loader = new GLTFLoader();
loader.load(
this.customModel,
(gltf) => {
const object = gltf.scene;
object.position.set(x, y, z);
this.scene.add(object);
},
undefined,
(error) => {
console.error("An error happened while loading the custom model", error);
}
);
},
importCustomModel(event) {
const file = event.target.files[0];
this.customModel = URL.createObjectURL(file);
},
showEditor() {
if (this.selectedObject) {
this.selectedObjectProps.color =
"#" + this.selectedObject.material.color.getHexString();
this.selectedObjectProps.posX = this.selectedObject.position.x;
this.selectedObjectProps.posY = this.selectedObject.position.y;
this.selectedObjectProps.posZ = this.selectedObject.position.z;
this.selectedObjectProps.scaleX = this.selectedObject.scale.x;
this.selectedObjectProps.scaleY = this.selectedObject.scale.y;
this.selectedObjectProps.scaleZ = this.selectedObject.scale.z;
this.selectedObjectProps.rotX = this.selectedObject.rotation.x;
this.selectedObjectProps.rotY = this.selectedObject.rotation.y;
this.selectedObjectProps.rotZ = this.selectedObject.rotation.z;
}
this.editorVisible = true;
},
applyEdit() {
if (this.selectedObject) {
this.selectedObject.material.color.set(this.selectedObjectProps.color);
this.selectedObject.position.set(
this.selectedObjectProps.posX,
this.selectedObjectProps.posY,
this.selectedObjectProps.posZ
);
this.selectedObject.scale.set(
this.selectedObjectProps.scaleX,
this.selectedObjectProps.scaleY,
this.selectedObjectProps.scaleZ
);
this.selectedObject.rotation.set(
this.selectedObjectProps.rotX,
this.selectedObjectProps.rotY,
this.selectedObjectProps.rotZ
);
}
this.editorVisible = false;
},
deleteBuilding() {
if (!this.selectedObject) {
console.log("No object selected for deletion.");
}
const selectedObjectId = this.selectedObject.id;
const rootParent = this.findRootParent(this.selectedObject);
const rootParentId = rootParent.id;
// 查找当前复杂模型
const currentModelIndex = this.scene.children.findIndex(
(item) => item.id === rootParentId
);
if (currentModelIndex === -1) {
console.error("Unable to find selected object in scene.");
return;
}
// 移除当前选中对象的模型
const currentModel = this.scene.children[currentModelIndex];
this.scene.remove(currentModel);
// 释放几何体
if (currentModel.geometry) {
currentModel.geometry.dispose();
}
// 释放材质
if (Array.isArray(currentModel.material)) {
currentModel.material.forEach((material) => material.dispose());
} else if (currentModel.material) {
currentModel.material.dispose();
}
// 从对象中移除引用
this.selectedObject = null;
// 更新场景渲染和控制器
this.renderer.render(this.scene, this.camera);
this.controls.update();
console.log("Selected object deleted:", this.selectedObject);
},
findRootParent(obj) {
if (!obj.parent || obj.parent instanceof THREE.Scene) {
// 如果当前对象没有parent属性,或者parent是场景(THREE.Scene)对象,说明已经是根节点了
return obj;
} else {
// 递归调用,继续向上查找
return this.findRootParent(obj.parent);
}
},
toggleBuildingMode() {
this.buildingMode = !this.buildingMode;
},
exportModelData() {
const modelData = this.scene.children
.filter((obj) => obj.type === "Mesh" && !obj.userData.isGround)
.map((obj) => ({
type: obj.geometry.type,
position: obj.position,
rotation: obj.rotation,
scale: obj.scale,
color: obj.material.color.getHex(),
}));
const blob = new Blob([JSON.stringify(modelData)], { type: "application/json" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "modelData.json";
link.click();
},
importModelData(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
const modelData = JSON.parse(e.target.result);
this.loadModelData(modelData);
};
reader.readAsText(file);
},
loadModelData(modelData = null) {
if (!modelData) {
return;
}
modelData.forEach((data) => {
let geometry;
switch (data.type) {
case "SphereGeometry":
geometry = new THREE.SphereGeometry(0.5, 32, 32);
break;
case "CylinderGeometry":
geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32);
break;
case "BoxGeometry":
default:
geometry = new THREE.BoxGeometry(1, 1, 1);
break;
}
const material = new THREE.MeshStandardMaterial({ color: data.color });
const object = new THREE.Mesh(geometry, material);
object.position.copy(data.position);
object.rotation.copy(data.rotation);
object.scale.copy(data.scale);
this.scene.add(object);
});
},
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialias;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#info {
width: 100%;
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 5px;
}
#editor {
position: absolute;
top: 50px;
right: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 5px;
z-index: 1000;
width: 200px;
}
#editor .form-group {
margin-bottom: 10px;
}
#editor label {
display: block;
margin-bottom: 5px;
}
#editor input {
width: 100%;
}
</style>
json文件
{
"buildingName": "大楼名称",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"floors": [
{
"floorNumber": 1,
"position": {
"x": 0,
"y": 0,
"z": 0
},
"rooms": [
{
"roomNumber": 101,
"position": {
"x": 0,
"y": 0,
"z": 0
},
"objects": [
{
"type": "floor",
"width": 10,
"depth": 10,
"height": 0.1,
"position": {
"x": 0,
"y": 0,
"z": 0
},
"color": "#808080"
},
{
"type": "wall",
"width": 10,
"height": 5,
"depth": 0.1,
"position": {
"x": 0,
"y": 2.5,
"z": 5
},
"color": "#ffffff"
},
{
"type": "wall",
"width": 10,
"height": 5,
"depth": 0.1,
"position": {
"x": 0,
"y": 2.5,
"z": -5
},
"color": "#ffffff"
},
{
"type": "wall",
"width": 0.1,
"height": 5,
"depth": 10,
"position": {
"x": 5,
"y": 2.5,
"z": 0
},
"color": "#ffffff"
},
{
"type": "wall",
"width": 0.1,
"height": 5,
"depth": 10,
"position": {
"x": -5,
"y": 2.5,
"z": 0
},
"color": "#ffffff"
},
{
"type": "door",
"position": {
"x": 2,
"y": 0,
"z": 5.1
},
"color": "#6e4b3f"
},
{
"type": "window",
"position": {
"x": -2,
"y": 2,
"z": 5.1
},
"color": "#87ceeb"
},
{
"type": "desk",
"position": {
"x": 1,
"y": 0,
"z": 1
},
"color": "#c0c0c0"
},
{
"type": "chair",
"position": {
"x": -1,
"y": 0,
"z": 1
},
"color": "#808080"
},
{
"type": "computer",
"position": {
"x": 0,
"y": 0.5,
"z": 1
},
"color": "#0000ff"
},
{
"type": "camera",
"position": {
"x": 0,
"y": 2,
"z": -5
},
"color": "#ff0000"
}
]
}
]
},
{
"floorNumber": 1,
"position": {
"x": 0,
"y": 5,
"z": 0
},
"rooms": [
{
"roomNumber": 12,
"position": {
"x": 0,
"y": 0,
"z": 0
},
"objects": [
{
"type": "floor",
"width": 10,
"depth": 10,
"height": 0.1,
"position": {
"x": 0,
"y": 0,
"z": 0
},
"color": "#808080"
},
{
"type": "wall",
"width": 10,
"height": 5,
"depth": 0.1,
"position": {
"x": 0,
"y": 2.5,
"z": 5
},
"color": "#ffffff"
},
{
"type": "wall",
"width": 10,
"height": 5,
"depth": 0.1,
"position": {
"x": 0,
"y": 2.5,
"z": -5
},
"color": "#ffffff"
},
{
"type": "wall",
"width": 0.1,
"height": 5,
"depth": 10,
"position": {
"x": 5,
"y": 2.5,
"z": 0
},
"color": "#ffffff"
},
{
"type": "wall",
"width": 0.1,
"height": 5,
"depth": 10,
"position": {
"x": -5,
"y": 2.5,
"z": 0
},
"color": "#ffffff"
},
{
"type": "door",
"position": {
"x": 2,
"y": 0,
"z": 5.1
},
"color": "#6e4b3f"
},
{
"type": "window",
"position": {
"x": -2,
"y": 2,
"z": 5.1
},
"color": "#87ceeb"
},
{
"type": "desk",
"position": {
"x": 1,
"y": 0,
"z": 1
},
"color": "#c0c0c0"
},
{
"type": "chair",
"position": {
"x": -1,
"y": 0,
"z": 1
},
"color": "#808080"
},
{
"type": "computer",
"position": {
"x": 0,
"y": 0.5,
"z": 1
},
"color": "#0000ff"
},
{
"type": "camera",
"position": {
"x": 0,
"y": 2,
"z": -5
},
"color": "#ff0000"
}
]
}
]
},
{
"floorNumber": 1,
"position": {
"x": 0,
"y": 10,
"z": 0
},
"rooms": [
{
"roomNumber": 12,
"position": {
"x": 0,
"y": 0,
"z": 0
},
"objects": [
{
"type": "floor",
"width": 10,
"depth": 10,
"height": 0.1,
"position": {
"x": 0,
"y": 0,
"z": 0
},
"color": "#808080"
},
{
"type": "wall",
"width": 10,
"height": 5,
"depth": 0.1,
"position": {
"x": 0,
"y": 2.5,
"z": 5
},
"color": "#ffffff"
},
{
"type": "wall",
"width": 10,
"height": 5,
"depth": 0.1,
"position": {
"x": 0,
"y": 2.5,
"z": -5
},
"color": "#ffffff"
},
{
"type": "wall",
"width": 0.1,
"height": 5,
"depth": 10,
"position": {
"x": 5,
"y": 2.5,
"z": 0
},
"color": "#ffffff"
},
{
"type": "wall",
"width": 0.1,
"height": 5,
"depth": 10,
"position": {
"x": -5,
"y": 2.5,
"z": 0
},
"color": "#ffffff"
},
{
"type": "door",
"position": {
"x": 2,
"y": 0,
"z": 5.1
},
"color": "#6e4b3f"
},
{
"type": "window",
"position": {
"x": -2,
"y": 2,
"z": 5.1
},
"color": "#87ceeb"
},
{
"type": "desk",
"position": {
"x": 1,
"y": 0,
"z": 1
},
"color": "#c0c0c0"
},
{
"type": "chair",
"position": {
"x": -1,
"y": 0,
"z": 1
},
"color": "#808080"
},
{
"type": "computer",
"position": {
"x": 0,
"y": 0.5,
"z": 1
},
"color": "#0000ff"
},
{
"type": "camera",
"position": {
"x": 0,
"y": 2,
"z": -5
},
"color": "#ff0000"
}
]
}
]
}
]
}
总结
研究路线应该错了 本章内容到此结束