最近项目需要在3D场景中给自定义的楼层区域进行贴图区分,对于普通的的纯色材质,实现比较简单,但是如果要进行纹理贴图的材质,就有点复杂了,这里写篇文章记录下。
首先看看我们的楼层定义,如何实现自定义区域。其实很简单,我们使用有序的点来定义楼层的平面形状,然后根据平面的定义,自动生成3d的平面区域。
var areaPts = [];
for (var idx = 0 ; idx < area.points.length; idx++) {
var p = area.points[idx];
var v = new THREE.Vector2(p.px , p.py );
areaPts.push(v);
}
var areaShape = new THREE.Shape(areaPts);
var geometry = new THREE.ShapeGeometry(areaShape);
如果是纯色的贴图我们怎么做,很简单直接设置颜色即可
var material = new THREE.MeshBasicMaterial({ color: color, side: THREE.DoubleSide, transparent: true, opacity: opacity });
var mesh = new THREE.Mesh(geometry, material);
对于贴图,我们使用同样的方法,看看会的到什么效果呢
var texture = new THREE.CanvasTexture(canvas);
var material = new THREE.MeshPhongMaterial({ map: texture, side: THREE.DoubleSide });
这里使用canvas作为贴图生成材质,运行后,非常不幸,你不会看到正常的贴图。这是为什么呢?原来我们的模型是根据一个shape生成的ShapeGeometry,所以贴图会采用UV坐标进行贴图,关于UV的解释可以看看这篇文章。所以我们需要计算模型的uv坐标供材质贴图使用。
function assignUVs(geometry) {
geometry.computeBoundingBox();
var max = geometry.boundingBox.max,
min = geometry.boundingBox.min;
var offset = new THREE.Vector2(0 - min.x, 0 - min.y);
var range = new THREE.Vector2(max.x - min.x, max.y - min.y);
var faces = geometry.faces;
geometry.faceVertexUvs[0] = [];
for (var i = 0; i < faces.length ; i++) {
var v1 = geometry.vertices[faces[i].a],
v2 = geometry.vertices[faces[i].b],
v3 = geometry.vertices[faces[i].c];
geometry.faceVertexUvs[0].push([
new THREE.Vector2((v1.x + offset.x) / range.x, (v1.y + offset.y) / range.y),
new THREE.Vector2((v2.x + offset.x) / range.x, (v2.y + offset.y) / range.y),
new THREE.Vector2((v3.x + offset.x) / range.x, (v3.y + offset.y) / range.y)
]);
}
geometry.uvsNeedUpdate = true;
}
算好geomotry的uv坐标后,我们就可以放心大胆的进行贴图了。
function getColRowMaterial(mesh)
{
var geometry = mesh.geometry;
assignUVs(geometry);
var area = mesh.userData.area;
geometry.computeBoundingBox();
var canvas = getColRowCanvas(area.rows, area.cols, geometry.boundingBox.size().x, geometry.boundingBox.size().y);
var texture = new THREE.CanvasTexture(canvas);
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.y = -1;
var material = new THREE.MeshPhongMaterial({ map: texture, side: THREE.DoubleSide });
mesh.material = material;
}
需要注意的是我们的图片坐标和uv坐标的Y轴是反的图片Y轴是向下的,UV坐标Y轴是向上的。所以我们需要反向下y。
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.y = -1;
经过以上的代码我们就可以得到正确的贴图了。
在线demo
<html>
<head>
<title>Three.js Geometry Texture</title>
<style>
body {
margin: 0;
}
canvas {
width: 100%;
height: 100%
}
</style>
<script src="js/three.min.js"></script>
<script src="js/OrbitControls.js"></script>
<script src="js/jQuery-2.1.4.min.js"></script>
</head>
<body>
<script>
var floorData;
var resizeRatio = 50;
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(0, 100, 100);
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x177FB3, 1);
document.body.appendChild(renderer.domElement);
var lightDirect = new THREE.DirectionalLight(0xf2f2f2, 0.8);//设置平行光源
lightDirect.position.set(0, 200, 0);//设置光源向量
scene.add(lightDirect);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
addFloor();
render();
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
;
function addFloor() {
$.getJSON("data/floorshape.json", function (result) {
floorData = result.data;
showAreas();
showWall();
});
}
function showWall() {
var wallgeometry = new THREE.BoxGeometry(1, 1, 1);
for (var i = 0; i < this.floorData.wall_segments.length; i++) {
var seg = this.floorData.wall_segments[i];
var start = this.floorData.wall_points[seg.start_idx];
var end = this.floorData.wall_points[seg.end_idx];
var a = new THREE.Vector2(start.px, start.py);
var b = new THREE.Vector2(end.px, end.py);
var v = b.sub(a);
var wall_width = v.length() * resizeRatio, wall_height = seg.height * resizeRatio, wall_depth = 2;
var material = new THREE.MeshBasicMaterial({color: 0x898989, transparent: true, opacity: 0.6});
var wall = new THREE.Mesh(wallgeometry, material);
wall.scale.x = wall_width;
wall.scale.y = wall_height;
wall.scale.z = wall_depth;
wall.position.x = (start.px + end.px) / 2 * resizeRatio
wall.position.y = wall_height / 2;
wall.position.z = -(start.py + end.py) / 2 * resizeRatio;
var angle = Math.atan2(v.y, v.x);
wall.rotateY(angle);
wall.castShadow = true;
scene.add(wall);
}
}
var areas = [];
function showAreas() {
for (var i = 0; i < this.floorData.areas.length; i++) {
var area = this.floorData.areas[i];
var areaPts = [];
for (var idx = 0; idx < area.points.length; idx++) {
var p = area.points[idx];
var v = new THREE.Vector2(p.px * resizeRatio, -p.py * resizeRatio);
areaPts.push(v);
}
var areaShape = new THREE.Shape(areaPts);
var geometry = new THREE.ShapeGeometry(areaShape);
var color = area.type == 2 ? '#729CB9' : area.color;
var h = area.type == 2 ? 0 : 0.05 * resizeRatio + Math.random() * 0.05 * resizeRatio;
var opacity = area.type == 2 ? 1 : 0.6;
var material = new THREE.MeshBasicMaterial({
color: color,
side: THREE.DoubleSide,
transparent: true,
opacity: opacity
});
var mesh = new THREE.Mesh(geometry, material);
mesh.rotateX(Math.PI / 2);
mesh.position.set(area.px * resizeRatio, h, -area.py * resizeRatio);
mesh.receiveShadow = true;
scene.add(mesh);
mesh.userData.name = area.name;
mesh.userData.objType = "area";
mesh.userData.area = area;
areas.push(mesh);
if (area.rows && area.rows.length > 0) {
getColRowMaterial(area, geometry, mesh);
}
if (area.type == 2) {
//caculate main floor area and make the camera fix the size
var b = new THREE.Box3().setFromObject(mesh);
var camera_z = Math.max(b.max.z, b.max.x) / 0.9;
var camera_y = Math.tan(Math.PI / 5) * Math.abs(camera_z);
camera.position.set(b.max.x * 0.2, camera_y, camera_z);
camera.lookAt(new THREE.Vector3(0, 0, 0));
}
}
}
function getColRowMaterial(area, geometry, mesh) {
assignUVs(geometry);
geometry.computeBoundingBox();
var canvas = getColRowCanvas(area.rows, area.cols, geometry.boundingBox.size().x, geometry.boundingBox.size().y);
var texture = new THREE.CanvasTexture(canvas);
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.y = -1;
var material = new THREE.MeshPhongMaterial({map: texture, side: THREE.DoubleSide});
mesh.material = material;
}
function getColRowCanvas(rows, cols, sizeWidth, sizeHeight) {
var width = sizeWidth, height = sizeHeight;
var fixSize = 16;
if (rows.length * fixSize > sizeWidth) {
var s = rows.length * fixSize / sizeWidth;
width = rows.length * fixSize;
height = sizeHeight * s;
}
else if (cols.length * fixSize > sizeHeight) {
var s = cols.length * fixSize / sizeHeight;
width = sizeWidth * s;
height = cols.length * fixSize;
}
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var rowNumber = rows.length + 1;
var colNumber = cols.length + 1;
var rowStep = height / rows.length;
var colStep = width / cols.length;
//background
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#fefefe';
ctx.fillRect(0, 0, width, height);
var fontSize = parseInt(Math.min(rowStep, colStep) * 0.5)
ctx.fillStyle = '#2891FF';
ctx.font = fontSize + "px Arial";
//rows
ctx.beginPath();
for (var i = 0; i < rowNumber; i++) {
ctx.fillText(rows[i], 0, (i * rowStep) + rowStep * 0.8);
ctx.moveTo(0, i * rowStep);
ctx.lineTo(width, i * rowStep);
}
//columns
for (var j = 0; j < colNumber; j++) {
ctx.fillText(cols[j], j * colStep + colStep * 0.1, rowStep * 0.4);
ctx.moveTo(j * colStep, 0);
ctx.lineTo(j * colStep, height);
}
ctx.stroke();
return canvas;
}
function assignUVs(geometry) {
geometry.computeBoundingBox();
var max = geometry.boundingBox.max,
min = geometry.boundingBox.min;
var offset = new THREE.Vector2(0 - min.x, 0 - min.y);
var range = new THREE.Vector2(max.x - min.x, max.y - min.y);
var faces = geometry.faces;
geometry.faceVertexUvs[0] = [];
for (var i = 0; i < faces.length; i++) {
var v1 = geometry.vertices[faces[i].a],
v2 = geometry.vertices[faces[i].b],
v3 = geometry.vertices[faces[i].c];
geometry.faceVertexUvs[0].push([
new THREE.Vector2((v1.x + offset.x) / range.x, (v1.y + offset.y) / range.y),
new THREE.Vector2((v2.x + offset.x) / range.x, (v2.y + offset.y) / range.y),
new THREE.Vector2((v3.x + offset.x) / range.x, (v3.y + offset.y) / range.y)
]);
}
geometry.uvsNeedUpdate = true;
}
</script>
</body>
</html>