web 3d场景构建+three.js+室内围墙,仓库,楼梯,货架模型等,第一人称进入场景案例

翻到了之前的一个案例,基于three.js做的仓库布局模拟,地图元素除了大模型外,其他都是通过JSON数据解析动态生成的,例如墙体,柱子门口,地标等,集成了第一人称的插件可以第一人称进入场景有需要的可以下载看看,对想入门的朋友应该有一些参考价值。

/**

*创建自定义几何体

*输入参数几何体底面逆时针坐标组、几何体高度

* 目前只支持凸多边形 逆时针则连线,顺时针不连线

*/

function createCustomBufferGeometry(planeArr, height, color) {

let planes = planeArr;

let planes2 = [];

//组装顶面坐标

for (let i = 0; i < planes.length; i++) {

planes2.push(new THREE.Vector3(planes[i].x, planes[i].y + height, planes[i].z));

}

planes = planes.concat(planes2);

let arr = [];

//循环组成三角面

for (let i = 0; i < planes.length; i++) {

let j = i + 1, k2 = j + planes2.length;

let xLength = planes2.length;

if (j >= planes2.length && i < planes2.length) {

j = 0; k2 = j + planes2.length;

}

if (i >= planes2.length) {

if (j >= planes.length) {

j = planes2.length;

}

k2 = i - planes2.length;

xLength = planes.length;

}

for (let x = i + 2; x < xLength; x++) {

arr = arr.concat([planes[i].x, planes[i].y, planes[i].z]);

arr = arr.concat([planes[j].x, planes[j].y, planes[j].z]);

arr = arr.concat([planes[x].x, planes[x].y, planes[x].z]);

// if (((planes[j].x - planes[i].x) * (planes[x].z - planes[i].z) - (planes[x].x - planes[i].x) * ( planes[j].z - planes[i].z)) < 0) {

// arr = arr.concat([planes[i].x, planes[i].y, planes[i].z]);

// arr = arr.concat([planes[j].x, planes[j].y, planes[j].z]);

// arr = arr.concat([planes[x].x, planes[x].y, planes[x].z]);

// }

}

arr = arr.concat([planes[i].x, planes[i].y, planes[i].z]);

if (i < planes2.length) {

arr = arr.concat([planes[j].x, planes[j].y, planes[j].z]);

arr = arr.concat([planes[k2].x, planes[k2].y, planes[k2].z]);

} else {

arr = arr.concat([planes[k2].x, planes[k2].y, planes[k2].z]);

arr = arr.concat([planes[j].x, planes[j].y, planes[j].z]);

}

}

let bufferGeometry = new THREE.BufferGeometry();

let vertices = new Float32Array(arr);

// itemSize = 3 因为每个顶点都是一个三元组。

bufferGeometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));

bufferGeometry.computeFaceNormals();//计算法向量,会对光照产生影响

bufferGeometry.computeVertexNormals();//自动设置法向量

let material = new THREE.MeshLambertMaterial({ color: color });

let mesh = new THREE.Mesh(bufferGeometry, material);

_bufferGeometry = bufferGeometry;

//worldScene.add(mesh);

return mesh;

}

var _bufferGeometry;

/**

*地图数据坐标是左上角为原点开始的二维坐标系x,y,绘制以左上角开始

* web 3d坐标原点是屏幕中心点,绘制的时候也是以中心为相对位置

* MapXLength:地图最长距离,MapZLength 地图最宽距离

* 转换规则 3d.position.x = 2d.width/2 - maxWidth/2 +2d.position.x

* 3d.position.z = 2d.height/2 - maxHeight/2 +2d.position.z;

* 3d.position.y y轴高度 2D地图无需设置,默认为0,如果有高度, 3d.position.y = 2d.高度.y/2+2d.高度位置+MapYLength/2

*/

function handCoordinate(data) {

data.x = data.x / 10;

data.y = data.y / 10;

data.z = data.z / 10;

if (data.positionY)

data.positionY = data.positionY / 10;

else

data.positionY = 0;

data.width = data.width / 10;

data.height = data.height / 10;

data.x = data.width / 2 - MapXLength / 2 + data.x;

data.z = data.height / 2 - MapZLength / 2 + data.z;

if (data.groupOption) {

data.groupOption.offSetX = data.groupOption.offSetX / 10;

data.groupOption.offSetY = data.groupOption.offSetY / 10;

data.groupOption.offSetZ = data.groupOption.offSetZ / 10;

}

}

var _baseBox;

var _floorType;

//最底层的box

function createBaseBox(data,floorType) {

data.width = data.width / 10;

data.height = data.height / 10;

MapXLength = data.width;

MapZLength = data.height;

sizeRatio = MapXLength / MapXLength;

if(data.floorType){

_floorType = data.floorType;

if(!floorModels[floorType])

{

floorModels[floorType] = [];

}

}

var geometry = new THREE.BoxBufferGeometry(data.width, 1, data.height);

var material = new THREE.MeshLambertMaterial({ color: data.color });

var cube = new THREE.Mesh(geometry, material);

cube.position.set(0, 0, 0);

//cube.castShadow = true;//开启投影

cube.receiveShadow = true;//接收阴影

cube.geometry.computeBoundingBox();

_baseBox = cube.geometry.boundingBox;

clickObjects.push(cube);//加入点击对象组

worldScene.add(cube);

floorModels[floorType].push(cube);

//console.log(cube);

//地图标注

// worldScene.add(createTextTextureBySprite(data))

}

//创建几何体

function createBox(data,_floorType) {

handCoordinate(data);

var geometry = new THREE.BoxGeometry(data.width, data.y, data.height);

var material = new THREE.MeshLambertMaterial({ color: data.color, vertexColors: THREE.FaceColors });

var cube = new THREE.Mesh(geometry, material);

cube.castShadow = true;//开启投影

//cube.receiveShadow = true;//接收阴影

cube.position.set(data.x, 0.55 + data.positionY / 2 + data.y / 2, data.z);

var newMesh;

//几何体组合处理

if (data.bspMesh) {

newMesh = cube;

data.bspMesh.forEach(x => {

handCoordinate(x);

let tempMesh;

if (x.geometryType == 'box') {

tempGeometry = new THREE.BoxGeometry(x.width, x.y, x.height);

tempMesh = new THREE.Mesh(tempGeometry, new THREE.MeshLambertMaterial({ color: x.color }));

}

tempMesh.position.set(x.x, 0.55 + x.positionY / 2 + x.y / 2, x.z);

newMesh = bspMesh(x.type, newMesh, tempMesh);

})

} else {

}

let finalMesh;

if (newMesh) {

newMesh.castShadow = true;//开启投影

// worldScene.add(newMesh);

finalMesh = newMesh;

} else {

finalMesh = cube;

// worldScene.add(cube);

}

//多个相同模型组合

if (data.type && data.type == 'group') {

for (let i = 0; i < data.groupOption.total; i++) {

let tempMesh = finalMesh.clone();

if (data.groupOption.offSetX != 0) {

tempMesh.position.x = finalMesh.position.x + (data.width + data.groupOption.offSetX) * i;

}

if (data.groupOption.offSetY != 0) {

tempMesh.position.y = finalMesh.position.y + (data.y + data.groupOption.offSety) * i;

}

if (data.groupOption.offSetZ != 0) {

tempMesh.position.z = finalMesh.position.z + (data.height + data.groupOption.offSetZ) * i;

}

// tempMesh.position.set(

// ( data.width+tempMesh.position.x+data.groupOption.offSetX)*i,

// ( data.y+tempMesh.position.y+data.groupOption.offSetY)*i,

// (data.height+tempMesh.position.z+data.groupOption.offSetZ)*i);

worldScene.add(tempMesh);

floorModels[_floorType].push(tempMesh);

}

} else {

worldScene.add(finalMesh);

floorModels[_floorType].push(finalMesh);

}

//地图标注

let sprite = createTextureBySprite(data);

if (sprite != null)

worldScene.add(sprite);

}

//创建圆柱体

function createCylinder(data) {

handCoordinate(data);

var geometry = new THREE.CylinderGeometry(data.width / 2, data.width / 2, data.y, 32);

var material = new THREE.MeshLambertMaterial({ color: data.color, vertexColors: THREE.FaceColors });

var cylinder = new THREE.Mesh(geometry, material);

cylinder.position.set(data.x, 0.55 + data.positionY / 2 + data.y / 2, data.z);

for (let i = 0; i < 64; i++) {

geometry.faces[i].color = new THREE.Color('#004892');

}

worldScene.add(cylinder);

}

/**

* 创建网格

* @param {几何体对象} geometry

*/

function createMesh(geometry, color) {

if (!color) {

color = '#4685C6';

}

return new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: color }));

}

/**

* A,B模型type:'intersect' 交集 'union' 并集 subtract 差集

* @param {A模型} geometryA

* @param {B模型} geometryB

*/

function bspGeometry(type, geometryA, geometryB) {

//生成ThreeBSP对象

var a = new ThreeBSP(geometryA);

var b = new ThreeBSP(geometryB);

//进行算

var resultBSP;

if (type == 'intersect')

resultBSP = a.intersect(b);

else if (type == 'union')

resultBSP = a.union(b);

else

resultBSP = a.subtract(b);

//从BSP对象内获取到处理完后的mesh模型数据

var result = resultBSP.toGeometry();

//更新模型的面和顶点的数据

result.computeFaceNormals();

result.computeVertexNormals();

return cresult;

}

/**

* A,B模型type:'intersect' 交集 'union' 并集 subtract 差集

* @param {A模型} meshA

* @param {B模型} meshB

*/

function bspMesh(type, meshA, meshB) {

//生成ThreeBSP对象

var a = new ThreeBSP(meshA);

var b = new ThreeBSP(meshB);

//进行算

var resultBSP;

if (type == 'intersect')

resultBSP = a.intersect(b);

else if (type == 'union')

resultBSP = a.union(b);

else

resultBSP = a.subtract(b);

//从BSP对象内获取到处理完后的mesh模型数据

var result = resultBSP.toMesh();

result.material = meshA.material;

//更新模型的面和顶点的数据

result.geometry.computeFaceNormals();

result.geometry.computeVertexNormals();

testResult = result

return result;

}

var testResult;

/**

*创建地图标注

*canvas地图标注的内容很小需要放大,放大会失真,后期调整其缩放大小,或者不采用canvas渲染

*/

function createTextureBySprite(data) {

if ((data.title == '' && data.imageurl == '') || (!data.title && !data.imageurl)) {

return null;

}

let canvas = document.createElement('canvas');

canvas.width=3000;

canvas.height=2000;

let ctx = canvas.getContext('2d');

ctx.lineWidth = 1;

ctx.textAlign = "center";

ctx.textBaseline = "middle";

ctx.textAlign = 'center';

if (data.font) {

ctx.font = data.font;

} else {

ctx.font = "Normal 180px Arial"

}

if (data.textcolor) {

ctx.fillStyle = data.textcolor;

}

//ctx.lineWidth = 4;

if (data.imageurl) {

let img = new Image();

img.src = data.imageurl;

img.onload = function () {

ctx.drawImage(img, 30, 90);

texture.needsUpdate = true;

}

}

/*

把整个 canvas 作为纹理,所以字尽量大一些,撑满整个 canvas 画布。

但也要小心文字溢出画布。

*/

ctx.fillText(data.title, 400, 200);

let texture = new THREE.CanvasTexture(canvas);

let material = new THREE.SpriteMaterial({

map: texture,

transparent: true, // 避免遮挡其他图形

// sizeAttenuation:false

});

let textMesh = new THREE.Sprite(material);

/*

精灵很小,要放大

*/

textMesh.scale.set(10, 10, 10);

/*

WebGL 3D 世界中的位置

*/

textMesh.position.set(data.x,data.y + 1.5, data.z);//data.y + 3

return textMesh;

}

相关推荐
燃先生._.32 分钟前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖1 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235242 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240252 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人3 小时前
前端知识补充—CSS
前端·css
GISer_Jing3 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245523 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v4 小时前
webpack最基础的配置
前端·webpack·node.js