2021年06月05日 - WebVR・Three.js
Three.jsでペーパーアニメーション
「How to Unroll Images with Three.js」を参考に、Three.jsで紙を広げるようなペーパーアニメーションを試してみました。※Three.jsはr129を使用しています。
Three.jsでペーパーアニメーション
● 平面を生成
PlaneGeometryで、ワイヤーフレーム表示にした平面を生成します。マテリアルは、「平面にシェーダを設定」でやったようにRawShaderMaterialを設定します。
また、uniformsにはアニメーションで使用するprogressとangleを設定します。
const uniforms = { //進行状況 progress:{type:'f',value:0.0}, //紙を広げる角度 angle:{type:'f',value:0.0}, }; const geometry = new THREE.PlaneGeometry(1,1,80,80); const material = new THREE.RawShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, uniforms:uniforms, wireframe:true, side:THREE.DoubleSide, }); const plane = new THREE.Mesh(geometry,material); scene.add(plane);
● 紙を広げるようなアニメーション
「How to Unroll Images with Three.js」を参考に、バーテックスシェーダで頂点の位置座標を計算します。
三角関数を使用して、アニメーション後の位置座標を計算し、進行状況の数値を反映することより紙を広げるようなアニメーションを実装します。
● glsl.js
//バーテックスシェーダ const vertexShader =` precision highp float; attribute vec3 position; attribute vec2 uv; uniform mat4 projectionMatrix; uniform mat4 modelViewMatrix; //紙を広げる角度 uniform float angle; //進行状況 uniform float progress; //行列による回転 mat4 rotationMatrix(vec3 axis, float angle){ axis = normalize(axis); float s = sin(angle); float c = cos(angle); float oc = 1.0 - c; return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0, oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0, oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0, 0.0,0.0,0.0,1.0); } //回転 vec3 rotate(vec3 v, vec3 axis, float angle){ mat4 m = rotationMatrix(axis, angle); return (m * vec4(v,1.0)).xyz; } void main(void){ float pi = 3.14159265359; //最終的な角度を計算 float finalAngle = angle - 0.0 * 0.3 * sin(progress * 6.0); vec3 newPosition = position; float rad = 0.1; float rolls = 8.0; //アニメーション後の位置座標を計算 newPosition = rotate(newPosition - vec3(-0.5,0.5,0.0),vec3(0.0,0.0,1.0),-finalAngle) + vec3(-0.5,0.5,0.0); float offs = (newPosition.x + 0.5) / (sin(finalAngle) + cos(finalAngle)); float tProgress = clamp((progress - offs * 0.99) / 0.01,0.0,1.0); newPosition.z = rad + rad * (1.0 - offs/2.0) * sin(-offs * rolls * pi - 0.5 * pi); newPosition.x = -0.5 + rad * (1.0 - offs/2.0) * cos(-offs * rolls * pi + 0.5 * pi); newPosition = rotate(newPosition - vec3(-0.5,0.5,0.0),vec3(0.0,0.0,1.0),finalAngle) + vec3(-0.5,0.5,0.0); newPosition = rotate(newPosition - vec3(-0.5,0.5,rad),vec3(sin(finalAngle),cos(finalAngle),0.0),-pi*progress*rolls); newPosition += vec3( -0.5 + progress * cos(finalAngle) * (sin(finalAngle) + cos(finalAngle)), 0.5 - progress * sin(finalAngle) * (sin(finalAngle) + cos(finalAngle)), rad * (1.0 - progress/2.0) ); //進行状況を反映 vec3 finalPosition = mix(newPosition,position,tProgress); gl_Position = projectionMatrix * modelViewMatrix * vec4(finalPosition,1.0); } `; //フラグメントシェーダ const fragmentShader =` precision highp float; void main(void){ gl_FragColor = vec4(1.0,1.0,1.0,1.0); } `; export { vertexShader, fragmentShader };
進行状況の数値を増やすことで、アニメーションさせます。
function rendering(){ requestAnimationFrame(rendering); time++; plane.material.uniforms.progress.value = time * 0.008; renderer.render(scene,camera); }
● テクスチャ画像
白い紙だと面白くないので、テクスチャ用にPIXTAで地図画像を購入しました。
● テクスチャとライトを設定
RawShaderMaterialにテクスチャとライトを設定しました。詳細は、「PlaneGeometryをシェーダで波形アニメーション」と「RawShaderMaterialにライトを設定」を参考にしてください。
● script.js
必要なライブラリを読み込みます。
<script src="js/preloadjs.min.js"></script> <script src="js/TweenMax.min.js"></script> <script src="js/script.js" type="module"></script>
完成したscript.jsになります。
//=============================================================== // Import Library //=============================================================== import * as THREE from './lib/three_jsm/three.module.js'; import { OrbitControls } from './lib/three_jsm/OrbitControls.js'; import { scene, camera, container, renderer } from './lib/basescene.js'; import { vertexShader, fragmentShader } from './glsl.js'; //=============================================================== // Init //=============================================================== window.addEventListener('load',function(){ init(); }); let orbitControls; let texture; let plane; let time = 0; function init(){ setLoading(); } function setLoading(){ TweenMax.to('.loader',0.1,{opacity:1}); const manifest = [ {id:'map',src:'./img/map.jpg'} ]; const loadQueue = new createjs.LoadQueue(); loadQueue.on('progress',function(e){ const progress = e.progress; }); loadQueue.on('complete',function(){ const image = loadQueue.getResult('map'); texture = new THREE.Texture(image); texture.needsUpdate = true; TweenMax.to('#loader_wrapper',1,{ opacity:0, onComplete:function(){ document.getElementById('loader_wrapper').style.display ='none'; TweenMax.to('.loader',0,{opacity:0}); } }); threeWorld(); setLight(); setControll(); rendering(); }); loadQueue.loadManifest(manifest); } //=============================================================== // Create World //=============================================================== function threeWorld(){ renderer.outputEncoding = THREE.sRGBEncoding; const uniforms = { progress:{type:'f',value:0.0}, angle:{type:'f',value:0.35}, texture:{type:'t',value:null}, diffuse:{type:'c',value:new THREE.Color(0xFFFFFF)}, emissive:{type:'c',value:new THREE.Color(0x000000)} }; const geometry = new THREE.PlaneGeometry(2,1,160,80); const material = new THREE.RawShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, uniforms:THREE.UniformsUtils.merge([ THREE.UniformsLib.lights, uniforms, ]), lights:true, side:THREE.DoubleSide, }); material.uniforms.texture.value = texture; plane = new THREE.Mesh(geometry,material); plane.rotation.x = -Math.PI / 2; scene.add(plane); } function setLight(){ const ambientlight = new THREE.AmbientLight(0xFFFFFF,0.1); scene.add(ambientlight); const pointLight = new THREE.PointLight(0XFFFFFF,10.0,2.15,1.0); pointLight.position.set(0,2,0); scene.add(pointLight); //const pointLightHelper = new THREE.PointLightHelper(pointLight,0.1); //scene.add(pointLightHelper); } function setControll(){ document.addEventListener('touchmove',function(e){e.preventDefault();},{passive:false}); orbitControls = new OrbitControls(camera,renderer.domElement); orbitControls.enableDamping = true; orbitControls.dampingFactor = 0.5; } function rendering(){ requestAnimationFrame(rendering); if(orbitControls){ orbitControls.update(); } time++; if(plane.material.uniforms.progress.value <= 1.5){ plane.material.uniforms.progress.value = time * 0.008; } renderer.render(scene,camera); }
● glsl.js
const vertexShader =` precision highp float; attribute vec3 position; attribute vec3 normal; attribute vec2 uv; uniform mat4 projectionMatrix; uniform mat4 modelViewMatrix; uniform mat3 normalMatrix; uniform float angle; uniform float progress; varying vec2 vUv; varying vec3 vViewPosition; varying vec3 vNormal; mat4 rotationMatrix(vec3 axis, float angle){ axis = normalize(axis); float s = sin(angle); float c = cos(angle); float oc = 1.0 - c; return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0, oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0, oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0, 0.0,0.0,0.0,1.0); } vec3 rotate(vec3 v, vec3 axis, float angle){ mat4 m = rotationMatrix(axis, angle); return (m * vec4(v,1.0)).xyz; } void main(void){ vUv = uv; vNormal = normalMatrix * normal; float pi = 3.14159265359; float finalAngle = angle - 0.0 * 0.3 * sin(progress * 6.0); vec3 newPosition = position; float rad = 0.1; float rolls = 8.0; newPosition = rotate(newPosition - vec3(-0.5,0.5,0.0),vec3(0.0,0.0,1.0),-finalAngle) + vec3(-0.5,0.5,0.0); float offs = (newPosition.x + 0.5) / (sin(finalAngle) + cos(finalAngle)); float tProgress = clamp((progress - offs * 0.99) / 0.01,0.0,1.0); newPosition.z = rad + rad * (1.0 - offs/2.0) * sin(-offs * rolls * pi - 0.5 * pi); newPosition.x = -0.5 + rad * (1.0 - offs/2.0) * cos(-offs * rolls * pi + 0.5 * pi); newPosition = rotate(newPosition - vec3(-0.5,0.5,0.0),vec3(0.0,0.0,1.0),finalAngle) + vec3(-0.5,0.5,0.0); newPosition = rotate(newPosition - vec3(-0.5,0.5,rad),vec3(sin(finalAngle),cos(finalAngle),0.0),-pi*progress*rolls); newPosition += vec3( -0.5 + progress * cos(finalAngle) * (sin(finalAngle) + cos(finalAngle)), 0.5 - progress * sin(finalAngle) * (sin(finalAngle) + cos(finalAngle)), rad * (1.0 - progress/2.0) ); vec3 finalPosition = mix(newPosition,position,tProgress); vec4 mvPosition = modelViewMatrix * vec4(finalPosition, 1.0); vViewPosition = mvPosition.xyz; gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader =` precision highp float; uniform vec3 diffuse; uniform vec3 emissive; uniform sampler2D texture; uniform mat4 viewMatrix; varying vec2 vUv; varying vec3 vViewPosition; varying vec3 vNormal; #include <common> #include <bsdfs> #include <lights_pars_begin> void main(void){ vec3 mvPosition = vViewPosition; vec3 transformedNormal = vNormal; GeometricContext geometry; geometry.position = mvPosition.xyz; geometry.normal = normalize(transformedNormal); geometry.viewDir = (normalize(-mvPosition.xyz)); vec3 lightFront = vec3(0.0); vec3 indirectFront = vec3(0.0); IncidentLight directLight; float dotNL; vec3 directLightColor_Diffuse; #if NUM_POINT_LIGHTS > 0 #pragma unroll_loop_start for (int i = 0; i < NUM_POINT_LIGHTS; i++) { getPointDirectLightIrradiance(pointLights[ i ], geometry, directLight); dotNL = dot(geometry.normal, directLight.direction); directLightColor_Diffuse = PI * directLight.color; lightFront += saturate(dotNL) * directLightColor_Diffuse; } #pragma unroll_loop_end #endif vec4 diffuseColor = vec4(diffuse, 1.0); ReflectedLight reflectedLight = ReflectedLight(vec3(0.0),vec3(0.0),vec3(0.0),vec3(0.0)); vec3 totalEmissiveRadiance = emissive; reflectedLight.indirectDiffuse = getAmbientLightIrradiance(ambientLightColor); reflectedLight.indirectDiffuse += indirectFront; reflectedLight.indirectDiffuse *= BRDF_Diffuse_Lambert(diffuseColor.rgb); reflectedLight.directDiffuse = lightFront; reflectedLight.directDiffuse *= BRDF_Diffuse_Lambert(diffuseColor.rgb); vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; vec3 color = texture2D(texture,vUv).rgb; gl_FragColor = vec4(color * outgoingLight,diffuseColor.a); } `; export { vertexShader, fragmentShader };
● basescene.js
sceneやcameraなど基本的な設定を管理するbasescene.jsです。
//=============================================================== // Import Library //=============================================================== import * as THREE from './three_jsm/three.module.js'; //=============================================================== // Base scene //=============================================================== let scene,camera,container,renderer; init(); function init(){ scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,100); camera.position.set(0,2,1.25); scene.add(camera); renderer = new THREE.WebGLRenderer({antialias:true}); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth,window.innerHeight); renderer.setClearColor(new THREE.Color(0x000000)); container = document.querySelector('#canvas_vr'); container.appendChild(renderer.domElement); window.addEventListener('resize',function(){ camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth,window.innerHeight); },false); } export { scene, camera, container, renderer }
完成したデモになります。Three.jsでペーパーアニメーションを試してみました。