1. 程式人生 > >Threejs ShapeGeometry自定義形狀貼圖

Threejs ShapeGeometry自定義形狀貼圖

最近專案需要在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>