今天為各位小夥伴推薦的是社區作者程序員秋風的文章,看看他如何用Three.js 來實現個人 VR 電影展廳~
1.Three.js系列: 寫一個第一/三人稱視角小遊戲
https://segmentfault.com/a/1190000041482998
2.Three.js系列: 造個海洋球池來學習物理引擎
https://segmentfault.com/a/1190000041888063
本文 gihtub 地址:https://github.com/hua1995116/Fly-Three.js
最近元宇宙的概念很火,並且受到疫情的影響,我們的出行總是受限,電影院也總是關門,但是在家裡又沒有看大片的氛圍,這個時候我們就可以通過自己來造一個宇宙,並在 VR 設備(Oculus 、cardboard)中來觀看。
今天我打算用 Three.js 來實現個人 VR 電影展廳,整個過程非常的簡單,哪怕不會編程都可以輕易掌握。
想要頂級的視覺盛宴,最重要的肯定是得要一塊大屏幕,首先我們就先來實現一塊大屏幕。
大屏幕的實現主要有兩種幾何體,一種是 PlaneGeometry 和 BoxGeometry,一個是平面,一個是六面體。為了使得屏幕更加有立體感,我選擇了 BoxGeometry。
老樣子,在添加物體之前,我們先要初始化我們的相機、場景和燈光等一些基礎的元件。
constscene=newTHREE.Scene();//相機constcamera=newTHREE.PerspectiveCamera(75,sizes.width/sizes.height,0.1,1000)camera.position.x=-5camera.position.y=5camera.position.z=5scene.add(camera);//添加光照constambientLight=newTHREE.AmbientLight(0xffffff,0.5)scene.add(ambientLight)constdirectionalLight=newTHREE.DirectionalLight(0xffffff,0.5)directionalLight.position.set(2,2,-1)scene.add(directionalLight)//控制器constcontrols=newOrbitControls(camera,canvas);scene.add(camera);然後來寫我們的核心代碼,創建一個 5 * 5 的超薄長方體
constgeometry=newTHREE.BoxGeometry(5,5,0.2);constcubeMaterial=newTHREE.MeshStandardMaterial({color:'#ff0000'});constcubeMesh=newTHREE.Mesh(geometry,cubeMaterial);scene.add(cubeMesh);效果如下:

然後緊接着加入我們的視頻內容,想要把視頻放入到3d場景中,需要用到兩樣東西,一個是 html 的 video 標籤,另一個是 Three.js 中的視頻紋理 VideoTexture
第一步將視頻標籤放入到 html 中,並設置自定播放以及不讓他顯示在屏幕中。
...<canvasclass="webgl"></canvas><videoid="video"src="./pikachu.mp4"playsinlinewebkit-playsinlineautoplayloopstyle="display:none"></video>...第二步,獲取到 video 標籤的內容將它傳給 VideoTexture,並且紋理賦給我們的材質。
+constvideo=document.getElementById('video');+consttexture=newTHREE.VideoTexture(video);constgeometry=newTHREE.BoxGeometry(5,5,0.2);constcubeMaterial=newTHREE.MeshStandardMaterial({-color:'#ff0000'+map:texture});constcubeMesh=newTHREE.Mesh(geometry,cubeMaterial);scene.add(cubeMesh);
我們看到皮神明顯被拉伸了,這裡就出現了一個問題就是紋理的拉伸。這也很好理解,我們的屏幕是 1 : 1 的,但是我們的視頻卻是 16:9 的。想要解決其實也很容易,要麼就是讓我們的屏幕大小更改,要麼就是讓我們的視頻紋理渲染的時候更改比例。
第一種方案很簡單
通過修改幾何體的形狀(也及時我們顯示器的比例)
constgeometry=newTHREE.BoxGeometry(8,4.5,0.2);constcubeMaterial=newTHREE.MeshStandardMaterial({map:texture});constcubeMesh=newTHREE.Mesh(geometry,cubeMaterial);scene.add(cubeMesh);第二種方案稍微有點複雜,需要知道一定的紋理貼圖相關的知識

圖1-1
首先我們先要知道紋理坐標是由 u 和 v 兩個方向組成,並且取值都為 0 - 1。通過在 fragment shader 中,查詢 uv 坐標來獲取每個像素的像素值,從而渲染整個圖。
因此如果紋理圖是一張16:9 的,想要映射到一個長方形的面中,那麼紋理圖必要會被拉伸,就像我們上面的視頻一樣,上面的圖為了表現出電視機的厚度所以沒有那麼明顯,可以看一下的圖。(第一張比較暗是因為 Three.js 默認貼圖計算了光照,先忽略這一點)


我們先來捋一捋,假設我們的圖片的映射是按照 圖1-1,拉伸的情況下 (80,80,0) 映射的是 uv(1,1 ),但是其實我們期望的是點(80, 80 9/16, 0) 映射的是 uv(1,1),所以問題變成了像素點位 (80, 80 9/16, 0) 的uv值 如何變成 (80, 80, 0) 的uv 值,更加簡單一些就是如何讓 80 9 / 16 變成 80,答案顯而易見,就是 讓 80 9 / 16 像素點的 v 值 乘以 16 / 9,這樣就能找到了 uv(1,1) 的像素值。然後我們就可以開始寫 shader 了。
//在頂點着色器傳遞uvconstvshader=`varyingvec2vUv;voidmain(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`//核心邏輯就是vec2uv=vUv*acept;constfshader=`varyingvec2vUv;uniformsampler2Du_tex;uniformvec2acept;voidmain(){vec2uv=vUv*acept;vec3color=vec3(0.3);if(uv.x>=0.0&&uv.y>=0.0&&uv.x<1.0&&uv.y<1.0)color=texture2D(u_tex,uv).rgb;gl_FragColor=vec4(color,1.0);}`
然後我們看到我們畫面已經正常了,但是在整體屏幕的下方,所以還差一點點我們需要將它移動到屏幕的中央。
移動到中央的思路和上面差不多,我們只需要注重邊界點,假設邊界點 C 就是讓 80( 0.5 + 9/160.5 ) 變成 80 ,很快我們也可能得出算是 C16/9 - 16/90.5 + 0.5 = 80
然後來修改 shader,頂點着色器不用改,我們只需要修改片段着色器。
constfshader=`varyingvec2vUv;uniformsampler2Du_tex;uniformvec2acept;voidmain(){vec2uv=vec2(0.5)+vUv*acept-acept*0.5;vec3color=vec3(0.0);if(uv.x>=0.0&&uv.y>=0.0&&uv.x<1.0&&uv.y<1.0)color=texture2D(u_tex,uv).rgb;gl_FragColor=vec4(color,1.0);}`好了,到現在為止,我們的圖像顯示正常啦~

那麼 Three.js 中的 textureVideo 到底是如何實現視頻的播放的呢?

通過查看源碼https://github.com/mrdoob/three.js/blob/6e897f9a42d615403dfa812b45663149f2d2db3e/src/textures/VideoTexture.js
源碼非常的少,VideoTexture 繼承了 Texture ,最大的一點就是通過requestVideoFrameCallback 這個方法,我們來看看它的定義,發現 mdn 沒有相關的示例,我們來到了 w3c 規範中尋找https://wicg.github.io/video-rvfc/
這個屬性主要是獲取每一幀的圖形,可以通過以下的小 demo 來進行理解
<body><videocontrols></video><canvaswidth="640"height="360"></canvas><span id="fps_text"/></body><script>functionstartDrawing(){varvideo=document.querySelector('video');varcanvas=document.querySelector('canvas');varctx=canvas.getContext('2d');varpaint_count=0;varstart_time=0.0;varupdateCanvas=function(now){if(start_time==0.0)start_time=now;ctx.drawImage(video,0,0,canvas.width,canvas.height);varelapsed=(now-start_time)/1000.0;varfps=(++paint_count/elapsed).toFixed(3);document.querySelector('#fps_text').innerText='videofps:'+fps;video.requestVideoFrameCallback(updateCanvas);}video.requestVideoFrameCallback(updateCanvas);video.src="http://example.com/foo.webm"video.play()}</script>通過以上的理解,可以很容易抽象出整個過程,通過 requestVideoFrameCallback 獲取視頻每一幀的畫面,然後用 Texture 去渲染到物體上。

然後我們來加入 VR 代碼, Three.js 默認給他們提供了建立 VR 的方法。
//step1引入VRButtonimport{VRButton}from'three/examples/jsm/webxr/VRButton.js';//step2將VRButton創造的dom添加進bodydocument.body.appendChild(VRButton.createButton(renderer));//step3設置開啟xrrenderer.xr.enabled=true;//step4修改更新函數renderer.setAnimationLoop(function(){renderer.render(scene,camera);});由於 iphone 太拉胯不支持 webXR ,特地借了台安卓機(安卓機需要下載 Google Play、Chrome 、Google VR),添加以上步驟後,就會如下顯示:

點擊 ENTER XR 按鈕後,即可進入 VR 場景。

然後我們我們可以再花20塊錢就可以買個谷歌眼鏡 cardboard。
體驗地址如下:https://fly-three-js.vercel.app/lesson03/code/index4.html

或者也可以像我一樣買一個 Oculus 然後躺着看大片

SegmentFault 思否社區小編說
自 2022-07-01 起 SegmentFault 思否公眾號改版啦!之後將陸續推出新的欄目和大家見面!(請拭目以待呀~❤)
在「社區精選」欄目中,我們將為廣大開發者推薦來自 SegmentFault 思否開發者社區的優質技術文章,這些文章全部出自社區中充滿智慧的技術創作者哦!
希望通過這一欄目,大家可以共同學習技術乾貨,GET 新技能和各種花式技術小 Tips。
歡迎越來越多的開發者加入創作者的行列,我們將持續甄選出社區中優質的內容推介給更多人,讓閃閃發光的技術創作者們走到聚光燈下,被更多人認識。
「社區精選」投稿郵箱:pr@segmentfault.com
投稿請附上社區文章地址
點擊左下角閱讀原文,到SegmentFault 思否社區和文章作者展開更多互動和交流,「公眾號後台「回復「入群」即可加入我們的技術交流群,收穫更多的技術文章~