WebGL初探—Three.js全景图实战

前段时间公司给了一个新需求就是写一个装修室内3D全景效果图,于是开始我的three.js开发之旅。
作为一个前端小白,突然接触three.js&webgl除了懵逼还是懵逼,不过作为一个技术人对于挑战也许就是软件开发中真正的乐趣,至少不会埋头调试一遍又一遍重复的页面数据,上上下下左左右右BABA......简直枯燥到极点。不过three.js&webgl不得不说给我打开了新的世界,接下来我就简单讲述一下我的学习之旅。

Three.js

Three.js 是一款运行在浏览器中的 3D 引擎,是JavaScript编写的WebGL第三方库,可以用它创建各种三维场景,包括了摄影机、光影、材质等各种对象,three.js内部也是webgl的封装,封装了大量了webgl API ,让比较繁琐的webgl更加简便。

WEBGL

WebGL(全写Web Graphics Library)是一种3D绘图协议,它让可以让开发进一步去了解图形渲染,Webgl是JavaScript和OpenGL ES 2.0合并出来的升级版,通过webgl可以让前端开发者们脱离开css渲染,可以了解更加底层的渲染,WebGL也可以为HTML5 Canvas提供硬件3D加速渲染,webgl是通过系统显卡来在浏览器里更流畅地展示3D场景和模型,加入shader(着色器)来对图形渲染,学习webgl需要具备相应的图形学算法,属于目前图形渲染开发的高级技术之一。目前webgl也运用在游戏,视频特效,包含untiy3D也是集成webgl。

技术讲解

three.js中主要由摄像机 ,场景 ,渲染器 , 资源加载器,素材组成

摄像机

webgl中的所有东西都是基于摄像机去展示的,可以利用摄像头的视角形成对3d视图观测视角,比如鱼眼视角,从而就让我们可以在平面图上可以开发出真实场景的3D视图。接下来我们看看怎么用three.js创建一个摄像机:

//新建一个相机
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1100);
camera.target = new THREE.Vector3(0, 0, 0);

场景

摄像机有了但是为了让景物可以更好的展现,这时候我们就需要一个展示景物的场景,three.js也为我们封装好了,如下所示可以创建一个场景:

var scene = new THREE.Scene();
scene.add(“资源”);

渲染器

渲染器是webgl的渲染启动开关,他可以调用render方式把场景渲染到摄像机。

var renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
//将场景渲染到摄像机
renderer.render(“场景”, “摄像机”);

资源加载器

three.js加载资源不同我们常见的html一样,直接通过src属性加载,而是通过TextureLoader.load来加载资源。

var textureLoader = new THREE.TextureLoader();
textureLoader .load(imgUrl);

素材

素材常见的包含网格,灯光等许多元素下面我就举个例子

var geometry = new THREE.SphereBufferGeometry(500, 60, 40); // invert the geometry on the x-axis so that all of the faces point inward
geometry.scale(-1, 1, 1);
//网格
var mesh = new THREE.Mesh(geometry, “shader组件”);

shader参数集

shader指令创建
<script id="vertexShader" type="x-shader/x-vertex">
varying vec2 vUv; 
void main() { 
vUv = uv; 
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
 gl_Position = projectionMatrix * mvPosition; 
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float; 
uniform float time; 
uniform float scale; 
uniform bool isoriginColor; 
uniform sampler2D texture3; 
uniform sampler2D texture4; 
varying vec2 vUv; 
void main( void ) { 
vec2 position = - 1.0 + 2.0 * vUv; 
vec4 color3 = texture2D( texture3, vUv ); 
vec3 tarcolor =color3.rgb; 
float f1 =color3.a*scale; 
vec4 color4 = texture2D( texture4, vUv ); 
float subscale=1.0-scale; 
float f2 =color4.a*subscale; 
if(isoriginColor == false){ 
tarcolor =mix(tarcolor.rgb,color4.rgb,f2); 
} 
gl_FragColor = vec4(tarcolor,1);
 }
</script>
调用指令
var uniforms = {
        time: {
            value: 1.0
            },
        scale: {
            value: 1.0
        },
        texture3: {
            value: getTextureLoader(0)
        },
          texture4: {
            value: getTextureLoader(1)
        }
  };
var material = new THREE.ShaderMaterial({
    uniforms: uniforms ,
    vertexShader: document.getElementById('vertexShader').textContent,
    fragmentShader: document.getElementById('fragmentShader').textContent
});

全部代码

应用组件包
<script type="text/javascript" src="js/three.min.js"></script>
<script type="text/javascript" src="js/D.min.js"></script>
<script type="text/javascript" src="js/doT.min.js"></script>
<script type="text/javascript" src="js/WebGL.js"></script>
<script type="text/javascript" src="js/OrbitControls.js"></script>
<script type="text/javascript" src="js/stats.min.js"></script>
html代码
<div class="haorooms_container">
<div id="content">
<div id="container"></div>
<img id="clickfile" src="img/icon_cell.png" style="display: none;position: absolute;bottom: 3rem;right: 1rem;" />
<input type="file" name="pano" id="pano" style="display: none;" />
<ul id="myList" class="imagelist">
<li><img id="0" class="meun_img" src="img/sun.jpg" /></li>
<li><img id="1" class="meun_img" src="img/banner1.jpg" /></li>
<li><img id="2" class="meun_img" src="img/banner2.jpg" /></li>
<li><img id="3" class="meun_img" src="img/banner3.jpg" /></li>
</ul></div>
<div class="dialogs-mask" id="dialog">
<div class="dialog">
    <div class="dialog-text-box">
    <h3 class="dialog-text-title">提示</h3>
    <p class="dialog-text-desc">是否添加标注?</p></div>
        <div class="dialog-btn-box">
        <button id="btnclose" class="dialog-btn dialog-text-close">取消</button>
        <button id="btncommit" class="dialog-btn dialog-text-commit">添加</button>
</div></div>
</div>
</div>
js代码
// 摄像机   ,场景   ,渲染器   ,   资源加载器
var camera, scene, renderer, textureLoader;
//图片集合
var imgs = ["img/sun.jpg", "img/banner1.jpg", "img/banner2.jpg", "img/banner3.jpg"];
//当前显示图片集合
var child = 0;
//是否启动混合替换
var isupdata = false;
//shader 参数
var uniforms;
//渲染计时
var interval = 0;
//递增混合阶段值
var count = 0;
var dialog, btnclose, btncommit;
var isUserInteracting = false,
onMouseDownMouseX = 0,
onMouseDownMouseY = 0,
lon = 0,
onMouseDownLon = 0,
lat = 0,
onMouseDownLat = 0,
phi = 0,
theta = 0;
init();
animate();
menuClick();
function menuClick() {
var list = document.getElementById('myList');
var listChild = document.getElementsByTagName('li');
for(var i = 0; i < listChild.length; i++) {
    listChild[i].addEventListener('click', function() {
    var id = this.children[0].id;
    if(child != id && count == 0) {
        uniforms.texture3.value = getTextureLoader(child);
        uniforms.texture4.value = getTextureLoader(id);
        child = id;
        isupdata = true;
        } else {}
        }, false);
    }
}
function init() {
var container;
dialog = document.getElementById("dialog");
btnclose = document.getElementById("btnclose");
btncommit = document.getElementById("btncommit");
container = document.getElementById('container');
//新建一个相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1100);
camera.target = new THREE.Vector3(0, 0, 0);
//创建一个WebGL渲染器
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
//新建一个场景
scene = new THREE.Scene();
//初始化加载器
                textureLoader = new THREE.TextureLoader();
                for(var i = 0, len = imgs.length; i < len; i++) {

                    setMeshChild(imgs[i]);
                }

                document.addEventListener('mousedown', onPointerStart, false);
                document.addEventListener('mousemove', onPointerMove, false);
                document.addEventListener('mouseup', onPointerUp, false);

                document.addEventListener('wheel', onDocumentMouseWheel, false);

                document.addEventListener('touchstart', onPointerStart, false);
                document.addEventListener('touchmove', onPointerMove, false);
                document.addEventListener('touchend', onPointerUp, false);
                document.addEventListener("dblclick", onDocumentMouseDown, false);
                //

                document.addEventListener('dragover', function(event) {

                    event.preventDefault();
                    event.dataTransfer.dropEffect = 'copy';

                }, false);

                document.addEventListener('dragenter', function() {

                    document.body.style.opacity = 0.5;

                }, false);

                document.addEventListener('dragleave', function() {

                    document.body.style.opacity = 1;

                }, false);

                //

                window.addEventListener('resize', onWindowResize, false);

            }

function showDialog(clicklistener) {
                console.log(dialog.style);
                if(dialog.style.display != "block") {
                    dialog.style.display = "block";
                }
                btncommit.addEventListener("click", function(event) {
                    event.preventDefault();
                    dialog.style.display = "none";
                    clicklistener(true);
                    return false;
                })
                btnclose.addEventListener("click", function(event) {
                    event.preventDefault();
                    dialog.style.display = "none";
                    clicklistener(false);
                    return false;
                })

            }
            /**
             * 获取三维坐标
             * @param {Object} event
             */
function onDocumentMouseDown(event) {
                event.preventDefault();
                var vector = new THREE.Vector3(); //三维坐标对象
                vector.set(
                    (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1,
                    0.5);
                vector.unproject(camera);
                var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
                var intersects = raycaster.intersectObjects(scene.children);
                if(intersects.length > 0) {
                    var selected = intersects[0]; //取第一个物体
                    console.log("x坐标:" + selected.point.x);
                    console.log("y坐标:" + selected.point.y);
                    console.log("z坐标:" + selected.point.z);
                    var x = selected.point.x;
                    var y = selected.point.y;
                    var z = selected.point.z;
                    showDialog(function(state) {
                        if(state) {
                            addLabelMarker(x, y, z, "img/icon_cell.png");
                        } else {
                            console.log("取消添加");
                        }
                    });

                }
            }
            /**
             * 
             * @param {Object} x
             * @param {Object} y
             * @param {Object} z
             * @param {Object} element
             * @param {Object} listener
             */
function addLabelMarker(x, y, z, imgUrl) {
                var map = new THREE.TextureLoader().load(imgUrl);
                map.wrapS = map.wrapT = THREE.RepeatWrapping;
                map.anisotropy = 16;
                var material = new THREE.MeshPhongMaterial({
                    map: map,
                    side: THREE.DoubleSide
                });
                        var geometry = new THREE.SphereBufferGeometry(20, 20, 20);
                        // invert the geometry on the x-axis so that all of the faces point inward
                        geometry.scale(-1, 1, 1);
                        var mesh = new THREE.Mesh(geometry, material);
                        mesh.position.set(x, y, z);
                        //添加灯光
                        var light = new THREE.PointLight(0xffffff, 1, 100);
                        light.position.set(x, y, z);
                        scene.add(light);
                        scene.add(mesh);
}
function setMeshChild(url) {
var geometry = new THREE.SphereBufferGeometry(500, 60, 40);
 // invert the geometry on the x-axis so that all of the faces point inward
                geometry.scale(-1, 1, 1);
                uniforms = {
                    time: {
                        value: 1.0
                    },
                    scale: {
                        value: 1.0
                    },
                    texture3: {
                        value: getTextureLoader(0)
                    },
                    texture4: {
                        value: getTextureLoader(1)
                    }
                };
                var material = new THREE.ShaderMaterial({
                    uniforms: uniforms,
                    vertexShader: document.getElementById('vertexShader').textContent,
                    fragmentShader: document.getElementById('fragmentShader').textContent
                });

                //网格
                var mesh = new THREE.Mesh(geometry, material);

                scene.add(mesh);

                document.addEventListener('drop', function(event) {

                    event.preventDefault();

                    var reader = new FileReader();
                    reader.addEventListener('load', function(event) {
                        material.map.image.src = event.target.result;
                        material.map.needsUpdate = true;

                    }, false);
                    reader.readAsDataURL(event.dataTransfer.files[0]);

                    document.body.style.opacity = 1;

                }, false);

            }

            function getTextureLoader(index) {
                return textureLoader.load(imgs[index]);
            }
            /**
             * 缩放大小
             */
            function onWindowResize(event) {

                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();

                renderer.setSize(window.innerWidth, window.innerHeight);

            }
            /**
             * 按下 鼠标 或则 触摸
             * @param {Object} event
             */
            function onPointerStart(event) {

                isUserInteracting = true;

                var clientX = event.clientX || event.touches[0].clientX;
                var clientY = event.clientY || event.touches[0].clientY;

                onMouseDownMouseX = clientX;
                onMouseDownMouseY = clientY;

                onMouseDownLon = lon;
                onMouseDownLat = lat;

            }
            /**
             * 移动
             * @param {Object} event
             */
            function onPointerMove(event) {

                if(isUserInteracting === true) {

                    var clientX = event.clientX || event.touches[0].clientX;
                    var clientY = event.clientY || event.touches[0].clientY;

                    lon = (onMouseDownMouseX - clientX) * 0.1 + onMouseDownLon;
                    lat = (clientY - onMouseDownMouseY) * 0.1 + onMouseDownLat;

                }

            }

            function onPointerUp() {

                isUserInteracting = false;

            }

            function onDocumentMouseWheel(event) {

                var fov = camera.fov + event.deltaY * 0.05;

                camera.fov = THREE.Math.clamp(fov, 10, 75);

                camera.updateProjectionMatrix();

            }

            function animate(timestamp) {
                requestAnimationFrame(animate);
                uniforms.time.value = timestamp / 1000;

                if(timestamp - interval > 200 && isupdata) {
                    if(count <= 20) {
                        var scale = 1.0 - (0.05 * count);
                          uniforms.scale.value = scale;
                        count++;
                    } else {
                        isupdata = false;
                        count = 0;
                    }
                    interval = timestamp
                }

                update();

            }

            function update() {

                if(isUserInteracting === false) {

                    lon += 0.1;

                }

                lat = Math.max(-85, Math.min(85, lat));
                phi = THREE.Math.degToRad(90 - lat);
                theta = THREE.Math.degToRad(lon);
                camera.target.x = 500 * Math.sin(phi) * Math.cos(theta);
                camera.target.y = 500 * Math.cos(phi);
                camera.target.z = 500 * Math.sin(phi) * Math.sin(theta);
                camera.lookAt(camera.target);
                //将场景渲染到摄像机
                renderer.render(scene, camera);

            }
####
演示地址

http://www.sunql.top/webgldemo/index.html

项目源码

https://github.com/sunql0827/webgldemo.git
https://gitee.com/sunql-hugh/webgldemo.git

总结

通过这次基于three.js的webgl全景图开发之旅为我对视图渲染打开了一道新的大门,不过webgl的厉害之处还有很多很多是我还未涉及到了,以后还需要更加努力了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341