一期链接:自定义编辑3D房间工具(一) 画线成墙
很久没更新了,打工人,沉迷于ctrl + C 、 ctrl + V;最近才有空,又捡起了 这东西。。。。
基本效果:
一、原理回顾
canvas 画图 ------> 产生数据 ------> 渲染3D
二、添加了新模块
玻璃墙、门、自定义地板
canvas部分
内 的地板、墙等用简单的 线(ctx.lineTo)、 方块(ctx.fillRect)实现;详情,请看一期。
这里特别要讲的是:画门
因为门,一定是在墙上的,且门的旋转角度一定和墙一致,所以画门的时候,不能像画墙一样。
它的交互应该是:点击墙上一点、则画出顺着墙方向的门,点击位置为门的中心位置。(点击位置需要根据墙的中心线位置修正,这样画出来的门才是墙里面居中的)
1、通过isPointInStroke 方法,判断 点击位置在哪个墙上,并使用该墙的参数
ctx.isPointInStroke(activePos[0], activePos[1])
2、修正门的坐标
// 获取门的坐标。点击位置,不一定是墙的中心线上的点,要简单处理下
const getDoorPos = (wallData: [[number, number], [number, number]], pos: [number, number]) => {
let result = pos;
const start = wallData[0];
const end = wallData[1];
if (end[0] - start[0] && end[1] - start[1]) {
// 点击点不在墙的中线上
// 墙函数
const k = (end[1] - start[1]) / (end[0] - start[0]);
const b = end[1] - k * end[0];
// 点击点垂线b值
const b2 = pos[1] + k * pos[0];
// 垂线和墙函数的交点
const y = (b + b2) / 2;
const x = (b2 - b) / (2 * k);
result = [x, y];
}
return result;
};
3、使用公共的画线的方法,画门
// 画门
if (activeNow?.type === 'wall') {
const params: any = {
id: new Date().getTime(),
type: active,
door: getDoorPos(activeNow.data, endPo),
wallId: activeNow.id,
data: activeNow.data,
};
lines.push(params);
}
const drawLine = (
ctx: any,
datas: [[number, number], [number, number]],
config?: {
type?: ActiveDrawType;
width?: number;
},
) => {
if (!datas || !ctx || !datas.length) {
return;
}
const { type = 'wall', width = 8 } = config || {};
const wallColorNow = wallColor[type];
const start = datas[0];
const end = datas[1];
ctx.beginPath(); //开始路径
ctx.lineWidth = width;
ctx.strokeStyle = wallColorNow;
ctx.setLineDash([]);
ctx.moveTo(start[0], start[1]); //定义路径起始点
ctx.lineTo(end[0], end[1]); //路径的去向
ctx.closePath();
ctx.stroke();
};
three部分
1、注意:bsp和threejs版本
我使用的:three版本:"@types/three": "^0.126.1",
bsp: 我用的是网上大佬自己改的。(非正式版本)我截取了部分关键的bsp调整代码,放到文章最后。
2、umi 与 threejs/fiber
我业务架构使用 umi ,因为umi 会对 prop.children进行统一处理。导致我threejs/fiber用不了!!! (这是我的部分吐槽)
bsp挖孔代码、基础墙代码
//墙上挖门,通过两个几何体生成BSP对象
function createResultBsp(bsp: any, less_bsp: any, mat: any) {
let material = wallGrayMaterial;
switch (mat) {
case 1:
material = wallPurpleMaterial;
break;
case 2:
material = wallGrayMaterial;
break;
}
const sphere1BSP = new ThreeBSP(bsp);
const cube2BSP = new ThreeBSP(less_bsp);
const resultBSP = sphere1BSP.subtract(cube2BSP);
const result = resultBSP.toMesh(material);
result.material.flatshading = THREE.FlatShading;
// result.geometry.computeFaceNormals(); //重新计算几何体侧面法向量
result.geometry.computeVertexNormals();
result.material.needsUpdate = true; //更新纹理
result.geometry.buffersNeedUpdate = true;
result.geometry.uvsNeedUpdate = true;
return result;
}
//返回墙对象
function returnWallObject(
width: any,
height: any,
depth: any,
angle: any,
material: any,
x: any,
y: any,
z: any,
) {
const cubeGeometry = new THREE.BoxGeometry(width, height, depth);
const cube = new THREE.Mesh(cubeGeometry, material);
cube.position.x = x;
cube.position.y = y;
cube.position.z = z;
cube.rotation.y = angle;
return cube;
}
// 有玻璃的镂空墙
function createGlassWall(datas: any[]) {
const { height = 0, glassHeight = 0, glassDepth = 1 } = configWall || {};
const left_wall = createCubeWall(datas, {
material: matArrayB,
});
const left_cube = createCubeWall(datas, {
material: matArrayB,
height: glassHeight,
y: (height - glassHeight) / 2,
});
const wallBsp = createResultBsp(left_wall, left_cube, 1);
const cube = createCubeWall(datas, {
material: glass_material,
height: glassHeight,
y: (height - glassHeight) / 2,
depth: glassDepth,
});
const bspGroup = new THREE.Group();
bspGroup.add(wallBsp);
bspGroup.add(cube);
return bspGroup;
}
三、模块选中及删除
核心思路:点击判断选中的模块,画选中线,点击按钮,批量删除
1、模块选中的判断
线的判断:ctx.isPointInStroke(activePos[0], activePos[1])
地板块的判断:containStroke(item.data, activePos[0], activePos[1])
const drawBoxs = ({
ctx,
lines,
activeLines,
activePos,
}: // endPo,
// active,
{
ctx: any;
lines: Record<string, any>[];
activeLines?: string[];
activePos?: any[];
endPo?: any[];
active?: string;
}) => {
let activeItem: any;
lines.forEach((item: any) => {
let func: any = drawLine;
const params: any = { type: item.type };
if (item.type === 'floor') {
func = drawFloor;
} else if (item.type === 'door') {
func = drawDoor;
params.door = item.door;
}
func(ctx, item.data, params);
// 判断当前选中线、或者地板
if (activePos && activePos.length) {
if (item.type === 'floor') {
if (containStroke(item.data, activePos[0], activePos[1])) {
activeItem = item;
}
} else if (ctx.isPointInStroke(activePos[0], activePos[1])) {
activeItem = item;
}
}
// 画选中的框
if (activeLines?.includes(item.id)) {
let start = item.data[0];
let end = item.data[1];
if (item.type === 'door') {
[start, end] = getDoorData(start, end, item.door);
}
drawBorder({
ctx,
isRect: item.type === 'floor',
start,
end,
});
}
});
return activeItem;
};
2、绘制选中虚线
原理:
已知 start、end 两个点坐标。已知墙宽度。
计算 a、b、c、d四个点坐标。
ok,使用我们的三角函数啥啥啥的。(可怜我一个社会老狗子,三角函数居然忘了)
// 绘制选中 虚线框
const drawBorder = (props: {
ctx: any;
lineWidth?: number;
strokeStyle?: string;
lineDash?: [number, number];
start: [number, number];
end: [number, number];
width?: number;
isRect?: boolean;
isDoor?: boolean;
}) => {
const {
ctx,
lineWidth = 2,
strokeStyle = '#f5222d',
lineDash = [6, 6],
width = 8,
start,
end,
isRect,
} = props || {};
ctx.beginPath(); //开始路径
ctx.lineWidth = lineWidth;
ctx.strokeStyle = strokeStyle;
ctx.setLineDash(lineDash);
// 地板块的,虚线框简单
if (isRect) {
ctx.strokeRect(start[0], start[1], end[0] - start[0], end[1] - start[1]);
} else {
let point1x = 0,
point1y = 0;
const width2 = width / 2;
if (end[0] - start[0]) {
// 墙的斜率,我们要使用的墙切面的斜率 -k
const k = (end[1] - start[1]) / (end[0] - start[0]);
const startb = start[1] + k * start[0];
// +(0, b)点,计算cos\sin然后计算,新的点相对原来点位移的x,y
const spoint0Y = start[1] - startb;
const spoint0StartXY = Math.pow(Math.pow(spoint0Y, 2) + Math.pow(start[0], 2), 0.5);
point1y = width2 * (start[0] / spoint0StartXY);
point1x = width2 * (spoint0Y / spoint0StartXY);
} else {
// 立着的时候 单独处理下
point1x = width2;
}
ctx.moveTo(start[0] + point1x, start[1] + point1y); //定义路径起始点
ctx.lineTo(start[0] - point1x, start[1] - point1y);
ctx.lineTo(end[0] - point1x, end[1] - point1y);
ctx.lineTo(end[0] + point1x, end[1] + point1y);
ctx.lineTo(start[0] + point1x, start[1] + point1y);
ctx.closePath();
}
ctx.stroke();
};
3、删除
删除,没啥好说的。我们一开始就将渲染和数据分离。
这里直接删除对应数据就好了。
四、结束
在座的各位大佬,新年快乐
BSP关键调整代码
ThreeBSP.prototype.toTree = function (treeIsh) {
if (treeIsh instanceof ThreeBSP.Node) {
return treeIsh;
}
// 看Three.js 0.126源码,各类Geometry基本都是继承或由BufferGeometry实现,基本上THREE.Geometry就废弃了,所以需要将获取点、法向量、uv信息要从以前的THREE.Geometry的faces中获取改为从THREE.BufferGeometry的attributes中获取
var polygons = [],
geometry =
treeIsh === THREE.BufferGeometry || (treeIsh.type || '').endsWith('Geometry')
? treeIsh
: treeIsh.constructor === THREE.Mesh
? (treeIsh.updateMatrix(), (this.matrix = treeIsh.matrix.clone()), treeIsh.geometry)
: void 0;
if (geometry && geometry.attributes) {
// TODO 暂时就不对geometry.attributes中的position、 normal和uv进行非空验证了,日后有时间在说吧,正常创建的BufferGeometry这些值通常都是有的
var attributes = geometry.attributes,
normal = attributes.normal,
position = attributes.position,
uv = attributes.uv;
// 点的数量
var pointsLength = attributes.position.array.length / attributes.position.itemSize;
// 如果索引三角形index不为空,则根据index获取面的顶点、法向量、uv信息
if (geometry.index) {
var pointsArr = [],
normalsArr = [],
uvsArr = [];
// 从geometry的attributes读取点、法向量、uv数据
for (var i = 0, len = pointsLength; i < len; i++) {
// 通常一个点和一个法向量的数据量(itemSize)是3,一个uv的数据量(itemSize)是2
var startIndex = 3 * i;
pointsArr.push(
new THREE.Vector3(
position.array[startIndex],
position.array[startIndex + 1],
position.array[startIndex + 2],
),
);
normalsArr.push(
new THREE.Vector3(
normal.array[startIndex],
normal.array[startIndex + 1],
normal.array[startIndex + 2],
),
);
uvsArr.push(new THREE.Vector2(uv.array[2 * i], uv.array[2 * i + 1]));
}
var index = geometry.index.array;
for (var i = 0, len = index.length; i < len; ) {
var polygon = new ThreeBSP.Polygon();
// 将所有面都按照三角面进行处理,即三个顶点组成一个面
for (var j = 0; j < 3; j++) {
var pointIndex = index[i],
point = pointsArr[pointIndex];
var vertex = new ThreeBSP.Vertex(
point.x,
point.y,
point.z,
normalsArr[pointIndex],
uvsArr[pointIndex],
);
vertex.applyMatrix4(this.matrix);
polygon.vertices.push(vertex);
i++;
}
polygons.push(polygon.calculateProperties());
}
} else {
// 如果索引三角形index为空,则假定每三个相邻位置(即相邻的三个点)表示一个三角形
for (var i = 0, len = pointsLength; i < len; ) {
var polygon = new ThreeBSP.Polygon();
// 将所有面都按照三角面进行处理,即三个顶点组成一个面
for (var j = 0; j < 3; j++) {
var startIndex = 3 * i;
var vertex = new ThreeBSP.Vertex(
position.array[startIndex],
position.array[startIndex + 1],
position.array[startIndex + 2],
new THREE.Vector3(
normal.array[startIndex],
normal.array[startIndex + 1],
normal.array[startIndex + 2],
),
new THREE.Vector2(uv.array[2 * i], uv.array[2 * i + 1]),
);
vertex.applyMatrix4(this.matrix);
polygon.vertices.push(vertex);
i++;
}
polygons.push(polygon.calculateProperties());
}
}
} else {
console.error('初始化ThreeBSP时为获取到几何数据信息,请检查初始化参数');
}
return new ThreeBSP.Node(polygons);
};