前段时间公司给了一个新需求就是写一个装修室内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的厉害之处还有很多很多是我还未涉及到了,以后还需要更加努力了。