簡單一招搞定 three.js 屏幕適配


這篇文章只討論 PerspectiveCamera 的適配方法

做過手機 H5 的同學可能會覺得屏幕適配挺麻煩。原因是設計師提供的設計稿尺寸比固定,但是前端開發者卻要適配不同大小、長寬比的目標設備。適配的終極目標無非是最大程度把主體內容優雅地呈現給用戶。開發和設計如果沒有協調好的話可能會妥協比較丑陋的方案,例如由於設計比例問題,為了照顧主體內容不被裁剪,只好設備兩邊,或者上下留黑邊這種。

不過在 3D 的世界里,我們不用擔心會有黑邊的問題,因為 3D 場景是無限延伸的,總能填滿任何比例的屏幕。

先看看 PerspectiveCamera 官方 API 說明如下:

PerspectiveCamera( fov, aspect, near, far ) fov — Camera frustum vertical field of view. aspect — Camera frustum aspect ratio. near — Camera frustum near plane. far — Camera frustum far plane.

上面四個參數都會影響成像結果,fov 和 aspect 設置 XY 平面的范圍,也就是廣度。 near 和 far 影響的是縱深 Z 軸的范圍,也就是深度。縱深只要保證物體離相機距離在這個范圍就可以了,這是為了性能而設置的參數,由用戶設置,只渲染必要的東西。實際上真實的相機這兩個值對應的是 0 到 無限遠。

這些參數設置好之后,成像就相應確定了。最后 three.js 把相機拍攝到的矩形區域對應好四個頂點渲染到屏幕上。同樣比例的屏幕看到的圖像是一致的,與屏幕大小無關</span>。

下面我用一個簡單的場景來看一下這些參數對成像的影響。

場景元素

  • 相機 (PerspectiveCamera)

  • 一個邊長為 100 的平面(主體內容范圍),放在世界坐標中心。

var camera = new THREE.PerspectiveCamera(53, 500 / 500, 0.1, 1000); var planeGemo = new THREE.PlaneGeometry( 100, 100, 10, 10 ) var meshMaterial = new THREE.MeshLambertMaterial(); meshMaterial.color = new THREE.Color(0x2dcaf1); meshMaterial.side = THREE.DoubleSide; var wireFrameMat = new THREE.MeshBasicMaterial(); wireFrameMat.color = new THREE.Color(0xdddddd); wireFrameMat.wireframe = true; var plane = THREE.SceneUtils.createMultiMaterialObject(planeGemo, [meshMaterial, wireFrameMat]); scene.add(plane);

目標

在任何屏幕下,都能最大程度地顯示完整的立方體。最大程度,就是最少多余空間的意思。下面是要達到效果

設置 fov 參數

可以直接想到的一種適配方法是——改變 camera 到目標物體的距離以控制成像的內容,但是這樣做計算成本比較高,而且還有可能影響其他一些數值,然后需要相應一起計算修改。
我想到改變視角也可以達到控制成像內容多少的目的,於是我想可不可以只通過改變 fov 一個數值,達到我要的效果。

fov 官網的定義翻譯過來是垂直方向的視角大小。我們先規定好相機到平面的距離為 100,然后試試看能不能通過計算設置 fov 值,剛好讓平面填滿一個寬高比為 1:1 的屏幕。

plane.position.set(0,0,0); camera.position.set(0,0,100); camera.lookAt(new THREE.Vector3);

觀察上面的圖,可以很容易求出 fov 的值, fov = arctan((100/2)/100) * 2; fov 為 0.9272952180016122,約等於 53 度。

camera.fov = Math.atan((100/2)/100) * 2 * (180 / Math.PI); camera.updateProjectionMatrix();

設置完剛剛求出的 fov 值,將場景渲染到 寬高比為 1:1 的畫布上。

渲染結果和預想的一樣,平面剛好填滿了 1:1 的畫布。

fov 和寬高比例的關系

下面在固定的 fov 下,使用 dat.gui 工具調整寬高比,觀察渲染區域的變化。

因為fov設置的是垂直方向的視角范圍,可以看到無論我們怎么改變寬高比例,垂直方向的渲染范圍,都是一致的。水平方向則是以裁剪的方式顯示。也就是說當我們設置好視角讓垂直方向范圍剛好等於主體內容的范圍,只要寬高比大於1,我們得到的渲染結果,已經是最佳的了。問題就只剩下當寬高比小於1的情況了。

寬高比小於1的時候,垂直方向顯示的高度剛好是等於主體內容的高度。為了能讓水平方向完整顯示主體內容,我們只有將垂直方向范圍增大,也就是將 fov 設置一個更大的值,此時水平方向的范圍也會隨之增大。當將 fov調整到 水平方向剛好能顯示主體內容時,垂直方向此時顯示的范圍是超過主體內容垂直方向的范圍的。其中的關系,其實可以用很簡單的函數求出來。

已知 照相機到主題內容的距離為 d
正方形主體內容的邊長為 w

設寬高比為 r,求照相機垂直方向的視角 f

當 r >= 1 時,照相機拍攝到的垂直方向范圍等於 w

當 r > 1
d * tan(f/2) * 2 = w

當 r < 1 時,照相機拍攝到的水平方向范圍等於 w,垂直方向范圍應該是 w/r
d * tan(f/2) * 2 = w/r

這樣,任意寬高比例的屏幕應該對應多大的垂直視角就確定了。

最終代碼與效果

var controls = new function () { camera.position.z = CAMERA_TO_MAIN_DIS; this.width = 500; this.height = 500; this.planeRY = 0; /** * 計算相機 fov 的函數 * @param d : 在相機前方 d 距離 * @param w : 想要看到最大正方形區域邊長為 w * @param r : 屏幕寬高比 */ function calcFov(d, w, r) { var f; var vertical = w; if (r < 1) { vertical = vertical/r; } f = Math.atan(vertical/d/2)*2 * (180 / Math.PI); return f; } this.redraw = ()=>{ webGLRenderer.setSize(this.width, this.height); plane.rotation.y = this.planeRY; camera.fov = calcFov(CAMERA_TO_MAIN_DIS, MAIN_CONTENT_WIDTH, this.width / this.height); camera.aspect = this.width / this.height; camera.updateProjectionMatrix(); } }

效果:

demo 的完整代碼:http://codepen.io/JasonTurbo/pen/ZLwJMo
原文鏈接:https://segmentfault.com/a/1190000008796468


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM