three.js尝试(一)模拟演唱会效果
工作闲暇之余,偶然翻到了Three.js的官网,立刻被它酷炫的案例给惊艳到了,当即下定决心要试验摸索一番,于是看demo,尝试,踩坑,解决问题,终于搞定了,一个模拟演唱会场景。
主角围绕一个钢管在舞动,两个聚光灯,来回摆动,后面屏幕上显示着照片,顶上一个立方体屏幕水平滚动,垂直滚动交替,后面荧光棒来回摆动,本来想搞一波荧光屏上图片定时切换的,摸索了一番效果不是很理想,就此作罢。先来看一下截图。
SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。需要注意的是,代码需要运行在服务器端,切记。
下面上代码:
<!DOCTYPE html> <html><head> <meta charset="UTF-8"> <style> </style> <title>测试</title> <script src="../js/three.js"></script> <script src="../js/dat.gui.min.js"></script> <script src="../js/OrbitControls.js"></script> <script src="../js/util.js"></script> </head>
<body> <script> var scene, camera, renderer, controls; var target1, target2; var angle = 0, flag = 1, lightFlag = 1; var sticks; // 荧光棒 var roles; // 主角 var textPlane, textCube, cubeRotationRange = true; var cubeArray = [ { text: "图片1", src: "../images/1.jpg", position: 'top' }, { text: "图片2", src: "../images/2.jpg", position: 'bottom' }, { text: "图片3", src: "../images/3.jpg", position: 'left' }, { text: "图片4", src: "../images/4.jpg", position: 'right' }, { text: "图片5", src: "../images/5.jpg", position: 'far' }, { text: "图片6", src: "../images/xusong.jpg", position: 'near' } ]; // textCube的六面 function init() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 500); camera.position.z = 200; camera.lookAt(scene.position); scene.add(camera); renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; // 这块儿踩了一个大坑,如果不设置这个,平行光束为毛是垂直屏幕照射的 renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap document.body.appendChild(renderer.domElement); controls = new THREE.OrbitControls(camera, renderer.domElement); controls.update(); window.addEventListener('resize', onWindowResize, false); // var axesHelper = new THREE.AxesHelper(10); // scene.add(axesHelper); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; renderer.setSize(window.innerWidth, window.innerHeight); } function run() { init(); roles = createBones(1, 5, 5, 8, true, false); //创建角色 bang = createBones(1, 0.5, 0.5, 8, false, false, new THREE.MeshStandardMaterial({ skinning: true, color: 0xccffff, // emissive: 0xccffff, side: THREE.DoubleSide, flatShading: THREE.FlatShading })); // 创建钢管 bang[0].add(roles[0]); // 让主角绕着钢管绕圈 createAmbientLight(); // 绘制环境光 createPlane(); // 创建舞台平面 createDirectionalLight(); // 平行光束 createTargets(); // 创建点光源跟踪 sticks = createBones(30, 0.1, 0.2, 2, false, true, new THREE.MeshPhongMaterial({ skinning: true, color: 0xffff66, emissive: 0xa72534, side: THREE.DoubleSide, flatShading: THREE.FlatShading })); // 创建荧光棒 createSpotlist(new THREE.Vector3(-50, 50, 0), target1); createSpotlist(new THREE.Vector3(50, 50, 0), target2); getTextCanvas(createTextPlane, [{ text: "许嵩", src: "../images/xusong.jpg", position: null }]); getTextCanvas(createTextCube, cubeArray); render(); }
function render() {
requestAnimationFrame(render); controls.update(); angle += flag * 1; angle = angle % 30; if (angle >= 29 || angle <= -29) { flag = -flag } roles[0].skeleton.bones[3].rotation.z = angle / 180 * Math.PI; roles[0].skeleton.bones[1].rotation.z = -angle / 180 * Math.PI; // 点光源跟随target移动 if (target1.position.x >= 0 || target1.position.x <= -40) { lightFlag = -lightFlag } target1.position.x += lightFlag * 0.5; target2.position.x += -lightFlag * 0.5; //荧光棒在各自的幅度内移动 for (let i = 0; i < sticks.length; i++) { let stick = sticks[i]; stick.skeleton.bones[0].rotation.z += 0.01 * stick.swingFlag; stick.positionNow += 0.01 * stick.swingFlag; if (Math.abs(stick.positionNow) > Math.abs(stick.swingRange)) { stick.swingFlag = -stick.swingFlag } } bang[0].rotation.y += 0.02; if (textCube) { if (cubeRotationRange) { textCube.rotation.y += 0.02; } else { textCube.rotation.x -= 0.02; } if (textCube.rotation.y > Math.PI * 2 || textCube.rotation.x < -Math.PI * 2) { textCube.rotation.x = 0; textCube.rotation.y = 0; cubeRotationRange = !cubeRotationRange; } } renderer.render(scene, camera); }
run();
function createBones(num, bigRadius, smallRadius, segmentHeight, isRole, isSticks, diyMaterial) { var meshes = [] //计算参数,这些参数在多处用到 var segmentHeight = segmentHeight; var segmentCount = 4; var height = segmentHeight * segmentCount; // 32 var halfHeight = height * 0.5; // 16
var sizing = { segmentHeight: segmentHeight, segmentCount: segmentCount, height: height, halfHeight: halfHeight }; for (let j = 0; j < num; j++) { //创建骨架 var bones = []; var prevBone = new THREE.Bone(); bones.push(prevBone); prevBone.position.y = - sizing.halfHeight; for (var i = 0; i < sizing.segmentCount; i++) { var bone = new THREE.Bone(); bone.position.y = sizing.segmentHeight; bones.push(bone); prevBone.add(bone); prevBone = bone;
} var skeleton = new THREE.Skeleton(bones);
//创建形状 // var geometry = new THREE.CylinderBufferGeometry( // bigRadius, // radiusTop // smallRadius, // radiusBottom // sizing.height, // height // 8, // radiusSegments // sizing.segmentCount * 3, // heightSegments // false // openEnded // ); var geometry = new THREE.CylinderGeometry( bigRadius, // radiusTop smallRadius, // radiusBottom sizing.height, // height 8, // radiusSegments sizing.segmentCount * 3, // heightSegments false // openEnded ); //将形状的每个点和骨骼建立关联,其中skinIndices指定该点由哪些骨骼控制(通过骨骼序号指定),skinWeights指定这些骨骼对该点的控制能力 for (var i = 0; i < geometry.vertices.length; i++) { var vertex = geometry.vertices[i]; var y = (vertex.y + sizing.halfHeight); var skinIndex = Math.floor(y / sizing.segmentHeight); var skinWeight = (y % sizing.segmentHeight) / sizing.segmentHeight; geometry.skinIndices.push(new THREE.Vector4(skinIndex, skinIndex + 1, 0, 0)); geometry.skinWeights.push(new THREE.Vector4(1 - skinWeight, skinWeight, 0, 0)); } var bufferGeometry = new THREE.BufferGeometry().fromGeometry(geometry); // 有自定义材料就用自定义材料,否则用默认的 var material = diyMaterial || new THREE.MeshPhongMaterial({ skinning: true, color: 0x156289, emissive: 0xa72534, side: THREE.DoubleSide, flatShading: THREE.FlatShading, wireframe: true }); var mesh = new THREE.SkinnedMesh(bufferGeometry, material);
//绑定骨架和网格,任务完成 mesh.add(bones[0]); mesh.bind(skeleton); mesh.scale.multiplyScalar(1); mesh.castShadow = true; meshes.push(mesh); scene.add(mesh);
if (isRole) { mesh.position.z = 10; }
if (isSticks) { let positionX = util.createRandomPos(-100, 100); let positionY = util.createRandomPos(0, 50); if (Math.abs(positionX) < 50) { positionY += 50; } mesh.position.set(positionX, positionY, -50); mesh.swingFlag = 1; mesh.swingRange = util.createRandomPos(Math.PI / 6, Math.PI / 2); mesh.positionNow = 0; }
// //SkeletonHelper可以用线显示出骨架,帮助我们调试骨架,可有可无 // skeletonHelper = new THREE.SkeletonHelper(mesh); // skeletonHelper.material.linewidth = 2; // scene.add(skeletonHelper); } return meshes; } function createAmbientLight() { var light = new THREE.AmbientLight(0x404040); // soft white light scene.add(light); } function createPlane() { //Create a plane that receives shadows (but does not cast them) var planeGeometry = new THREE.PlaneBufferGeometry(100, 100, 32, 32); var planeMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00 }) var plane = new THREE.Mesh(planeGeometry, planeMaterial); plane.rotation.x = -Math.PI / 2; plane.position.y = -20; plane.receiveShadow = true; scene.add(plane); } function createDirectionalLight() { var directionalLight = new THREE.DirectionalLight(0xffffff); directionalLight.position.set(0, 100, 0); //default; directionalLight shining from top directionalLight.castShadow = true; // default false
//Set up shadow properties for the directionalLight directionalLight.shadow.mapSize.width = 512; // default directionalLight.shadow.mapSize.height = 512; // default directionalLight.shadow.camera.left = -1; // default directionalLight.shadow.camera.right = 1; // default directionalLight.shadow.camera.top = 1; // default directionalLight.shadow.camera.bottom = -1; // default directionalLight.shadow.camera.near = 0.5; // default directionalLight.shadow.camera.far = 500; // default scene.add(directionalLight);
// //Create a helper for the shadow camera (optional) // var helper = new THREE.CameraHelper(directionalLight.shadow.camera); // scene.add(helper); } function createSpotlist(Vector3, target) { var spotLight = new THREE.SpotLight(0xffffff); spotLight.position.set(Vector3.x, Vector3.y, Vector3.z);
spotLight.castShadow = true; spotLight.angle = Math.PI / 18; spotLight.shadow.mapSize.width = 512; spotLight.shadow.mapSize.height = 512;
spotLight.shadow.camera.near = 0.5; spotLight.shadow.camera.far = 500; spotLight.shadow.camera.fov = 30; spotLight.target = target; scene.add(spotLight);
// Create a helper for the spotlight // var helper = new THREE.SpotLightHelper(spotLight); // scene.add(helper);
// //Create a helper for the shadow camera (optional) // var helper = new THREE.CameraHelper(spotLight.shadow.camera); // scene.add(helper); }
function createTargets() { target1 = new THREE.Object3D(); target1.position.set(-20, 0, 0);
scene.add(target1); target2 = new THREE.Object3D(); target2.position.set(20, 0, 0); scene.add(target2); }
function getTextCanvas(callback, srcList) { var canvasList = []; var imgList = []; var totalCount = srcList.length, loadedCount = 0; for (let i = 0; i < srcList.length; i++) { let img = new Image(); img.src = srcList[i].src; img.onload = function () { loadedCount++; imgList.push({ img: img, position: srcList[i].position, text: srcList[i].text }); } } // 开始处理回调函数 if (typeof callback == "function") { // 这里的this实际上指的是this对象 function check() { // if (loadedCount >= totalCount) { // 如果加载完了 for (let i = 0; i < imgList.length; i++) { var width = 512, height = 256; var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; // canvas.style.backgroundImage = "url('three/xusong.jpg')"; var ctx = canvas.getContext('2d'); ctx.fillStyle = '#C3C3C3'; // 背景颜色 ctx.fillRect(0, 0, width, height); ctx.drawImage(imgList[i].img, 0, 0, 512, 256); // callback(canvas); // 利用该canvas构建要用的物体 ctx.font = 50 + 'px " bold'; ctx.fillStyle = '#2891FF'; // 文字颜色 ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(imgList[i].text, width / 2, height / 2); canvasList.push({ canvas: canvas, position: imgList[i].position }); } // let createdObj = callback(canvasList); if (callback.name == 'createTextCube') { textCube = callback(canvasList); // console.log(cubeText); } else if (callback.name == 'createTextPlane') { textPlane = callback(canvasList); } } else { // 没有加载完毕 setTimeout(check, 100); } } // 开始反复检查图片有么有加载完毕 check(); } } function createTextPlane(canvasList) { //Create a plane that receives shadows (but does not cast them) var geometry = new THREE.PlaneGeometry(100, 50, 32); var texture = new THREE.Texture(canvasList[0].canvas); // canvas做纹理 texture.needsUpdate = true; var materials = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide }) // top // var materials = new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(canvas), side: THREE.DoubleSide }) // top var textPlane = new THREE.Mesh(geometry, materials); textPlane.position.z = -50; textPlane.receiveShadow = true; scene.add(textPlane); return textPlane; } function createTextCube(canvasList) { //Create a plane that receives shadows (but does not cast them) var geometry = new THREE.BoxGeometry(25, 25, 25); var colorList = ['blue', 'yellow', 'green', 'red']; var positionList = { 'right': 0, 'left': 1, 'top': 2, 'bottom': 3, 'near': 4, 'far': 5 }; var materials = []; for (let i = 0; i < canvasList.length; i++) { var texture = new THREE.Texture(canvasList[i].canvas); texture.needsUpdate = true; materials[positionList[canvasList[i].position]] = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide }); } for (let j = 0; j < 6; j++) { if (materials[j] && !materials[j].isMeshBasicMaterial) { materials[j] = new THREE.MeshBasicMaterial({ color: colorList[Math.floor(Math.random() * 4)] }); } } var textCube = new THREE.Mesh(geometry, materials); textCube.position.y = 50; textCube.receiveShadow = true; scene.add(textCube); return textCube; } </script> </body>
</html>
总结:本次小尝试学会了three.js的基本模型的画法、光源的用法、骨骼与模型的绑定、纹理等的基本用法。
不足之处:骨骼与模型各节点的绑定权重的计算关系不太理解。

更多精彩