摘要:首先,下載并在的中使用然后,我們需要準備一個模型,在函數(shù)中,創(chuàng)建變量,用于導入模型導入模型的時候,接受兩個參數(shù),第一個表示模型路徑,第二個表示完成導入后的回調(diào)函數(shù),一般我們需要在這個回調(diào)函數(shù)中將導入的模型添加到場景中。
9. 動畫
9.1 實現(xiàn)動畫效果 9.1.1 動畫原理在本章之前,所有畫面都是靜止的,本章將介紹如果使用Three.js進行動態(tài)畫面的渲染。此外,將會介紹一個Three.js作者寫的另外一個庫,用來觀測每秒幀數(shù)(FPS)。
在這里,我們將動態(tài)畫面簡稱為動畫(animation)。正如動畫片的原理一樣,動畫的本質(zhì)是利用了人眼的視覺暫留特性,快速地變換畫面,從而產(chǎn)生物體在運動的假象。而對于Three.js程序而言,動畫的實現(xiàn)也是通過在每秒鐘多次重繪畫面實現(xiàn)的。
為了衡量畫面切換速度,引入了每秒幀數(shù)FPS(Frames Per Second)的概念,是指每秒畫面重繪的次數(shù)。FPS越大,則動畫效果越平滑,當FPS小于20時,一般就能明顯感受到畫面的卡滯現(xiàn)象。
那么FPS是不是越大越好呢?其實也未必。當FPS足夠大(比如達到60),再增加幀數(shù)人眼也不會感受到明顯的變化,反而相應地就要消耗更多資源(比如電影的膠片就需要更長了,或是電腦刷新畫面需要消耗計算資源等等)。因此,選擇一個適中的FPS即可。
NTSC標準的電視FPS是30,PAL標準的電視FPS是25,電影的FPS標準為24。而對于Three.js動畫而言,一般FPS在30到60之間都是可取的。
9.1.2 setInterval方法如果要設置特定的FPS(雖然嚴格來說,即使使用這種方法,JavaScript也不能保證幀數(shù)精確性),可以使用JavaScript DOM定義的方法:
setInterval(fn,mesc)
其中,fn是每過msec毫秒執(zhí)行的函數(shù),如果將fn定義為重繪畫面的函數(shù),就能實現(xiàn)動畫效果。setInterval函數(shù)返回一個變量timer,如果需要停止重繪,需要使用clearInterval方法,并傳入該變量timer,具體的做法為:
1、首先,在init函數(shù)中定義每20毫秒執(zhí)行draw函數(shù)的setInterval,返回值記錄在全局變量timer中:
timer = setInterval(draw,20);
2、在draw函數(shù)中,我們首先設定在每幀中的變化(畢竟,如果每幀都是相同的,即使重繪再多次,還是不會有動畫的效果),這里我們讓場景中的長方體繞y軸轉(zhuǎn)動。然后,執(zhí)行渲染:
function draw() { // 每過20ms 就會執(zhí)行一次這個函數(shù),rotation.y就會加0.01 // 轉(zhuǎn)完360度就會進行取余,所以就會一直轉(zhuǎn)下去 mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2); renderer.render(scene, camera); }
這樣,每20毫秒就會調(diào)用一次draw函數(shù),改變長方體的旋轉(zhuǎn)值,然后進行重繪。最終得到的效果就是FPS為50的旋轉(zhuǎn)長方體。
3、我們在HTML中添加兩個按鈕,一個是按下后停止動畫,另一個是按下后繼續(xù)動畫:
4、對應的stop和start函數(shù)為:
function stop() { if (timer !== null) { clearInterval(timer); timer = null; } } function start() { if (timer == null) { clearInterval(timer); timer = setInterval(draw, 20); } }
完整代碼:
動畫效果
效果圖:
9.1.3 requestAnimationFrame方法大多數(shù)時候,我們并不在意多久重繪一次,這時候就適合用requestAnimationFrame方法了。它告訴瀏覽器在合適的時候調(diào)用指定函數(shù),通常可能達到60FPS。
requestAnimationFrame同樣有對應的cancelAnimationFrame取消動畫:
function stop() { if (timer !== null) { cancelAnimationFrame(timer); timer = null; } }
和setInterval不同的是,由于requestAnimationFrame只請求一幀畫面,因此,除了在init函數(shù)中需要調(diào)用,在被其調(diào)用的函數(shù)中需要再次調(diào)用requestAnimationFrame:
function draw() { mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2); renderer.render(scene, camera); timer = requestAnimationFrame(draw); }
因為requestAnimationFrame較為“年輕”,因而一些老的瀏覽器使用的是試驗期的名字:mozRequestAnimationFrame、webkitRequestAnimationFrame、msRequestAnimationFrame,為了支持這些瀏覽器,我們最好在調(diào)用之前,先判斷是否定義了requestAnimationFrame以及上述函數(shù):
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame;
完整代碼:
動畫效果
setInterval和requestAnimationFrame的區(qū)別:
setInterval方法與requestAnimationFrame方法的區(qū)別較為微妙。一方面,最明顯的差別表現(xiàn)在setInterval可以手動設定FPS,而requestAnimationFrame則會自動設定FPS;但另一方面,即使是setInterval也不能保證按照給定的FPS執(zhí)行,在瀏覽器處理繁忙時,很可能低于設定值。當瀏覽器達不到設定的調(diào)用周期時,requestAnimationFrame采用跳過某些幀的方式來表現(xiàn)動畫,雖然會有卡滯的效果但是整體速度不會拖慢,而setInterval會因此使整個程序放慢運行,但是每一幀都會繪制出來;
總而言之,requestAnimationFrame適用于對于時間較為敏感的環(huán)境(但是動畫邏輯更加復雜),而setInterval則可在保證程序的運算不至于導致延遲的情況下提供更加簡潔的邏輯(無需自行處理時間)。
9.2 使用stat.js記錄FPSstat.js是Three.js的作者Mr.Doob的另一個有用的JavaScript庫。很多情況下,我們希望知道實時的FPS信息,從而更好地監(jiān)測動畫效果。這時候,stat.js就能提供一個很好的幫助,它占據(jù)屏幕中的一小塊位置(如左上角),效果為:,單擊后顯示每幀渲染時間:。
首先,我們需要下載stat.js文件,可以在https://github.com/mrdoob/stats.js/blob/master/build/stats.min.js找到。下載后,將其放在項目文件夾下,然后在HTML中引用:
在頁面初始化的時候,對其初始化并將其添加至屏幕一角。這里,我們以左上角為例:
var stat = null; function init() { stat = new Stats(); stat.domElement.style.position = "absolute"; stat.domElement.style.left = "0px"; stat.domElement.style.top = "0px"; document.body.appendChild(stat.domElement); // Three.js init ... }
然后,在上一節(jié)介紹的動畫重繪函數(shù)draw中調(diào)用stat.begin();與stat.end();分別表示一幀的開始與結(jié)束:
function draw() { stat.begin(); mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2); renderer.render(scene, camera); stat.end(); }
完整代碼:
9.3 彈球案例stats
本節(jié)我們將使用一個彈球的例子來完整地學習使用動畫效果。
1、首先,我們把通用的框架部分寫好,按照之前的方法實現(xiàn)動畫重繪函數(shù),并加入stat.js庫:
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame; var stat; var renderer; var scene; var camera; var light; function init() { stat = new Stats(); stat.domElement.style.position = "absolute"; stat.domElement.style.left= "0px"; stat.domElement.style.top = "0px"; document.body.appendChild(stat.domElement); renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("mainCanvas") }); scene = new THREE.Scene(); timer = requestAnimationFrame(draw); } function draw() { stat.begin(); renderer.render(scene, camera); timer = requestAnimationFrame(draw); stat.end(); } function stop() { if (timer !== null) { cancelAnimationFrame(timer); timer = null; } }
2、然后,為了實現(xiàn)彈球彈動的效果,我們創(chuàng)建一個球體作為彈球模型,創(chuàng)建一個平面作為彈球反彈的平面。為了在draw函數(shù)中改變彈球的位置,我們可以聲明一個全局變量ballMesh,以及彈球半徑ballRadius。
var ballMesh; var ballRadius = 0.5;
3、在init函數(shù)中添加球體和平面,使彈球位于平面上,平面采用棋盤格圖像作材質(zhì):
// 加載貼圖 texture = THREE.ImageUtils.loadTexture("images/chess.png", {}, function() { renderer.render(scene, camera); }); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(4, 4); // 平面模型 var plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8), new THREE.MeshLambertMaterial({ map: texture })); // 沿x軸旋轉(zhuǎn)-90度 plane.rotation.x = Math.PI / -2; scene.add(plane); // 球模型 ballMesh = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 40, 16), new THREE.MeshLambertMaterial({ color: 0xffff00 }));
4、為了記錄彈球的狀態(tài),我們至少需要位置、速度、加速度三個矢量,為了簡單起見,這里彈球只做豎直方向上的自由落體運動,因此位置、速度、加速度只要各用一個變量表示。其中,位置就是ballMesh.position.y,不需要額外的變量,因此我們在全局聲明速度v和加速度a:
var v = 0; var a = -0.01;
這里,a = -0.01代表每幀小球向y方向負方向移動0.01個單位。
5、一開始,彈球從高度為maxHeight(自己定義的一個高度)處自由下落,掉落到平面上時會反彈,并且速度有損耗。當速度很小的時候,彈球會在平面上作振幅微小的抖動,所以,當速度足夠小時,我們需要讓彈球停止跳動。因此,定義一個全局變量表示是否在運動,初始值為false:
var isMoving = false;
6、在HTML中定義一個按鈕,點擊按鈕時,彈球從最高處下落:
7、下面就是最關(guān)鍵的函數(shù)了,在draw函數(shù)中,需要判斷當前的isMoving值,并且更新小球的速度和位置:
function draw() { stat.begin(); if (isMoving) { ballMesh.position.y += v; // a= -0.01 v += a; // 當小球從定義的高度落到小球停在平面時的高度的時候 if (ballMesh.position.y <= ballRadius) { // 讓小球彈起來 v = -v * 0.9; } // 當小球的速度小于設定值的時候 if (Math.abs(v) < 0.001) { // 讓它停下來 isMoving = false; ballMesh.position.y = ballRadius; } } renderer.render(scene, camera); requestAnimationFrame(draw); stat.end(); }
完整代碼:
彈彈彈
效果圖:
10. 外部模型前面我們了解到,使用Three.js創(chuàng)建常見幾何體是十分方便的,但是對于人或者動物這樣非常復雜的模型使用幾何體組合就非常麻煩了。因此,Three.js允許用戶導入由3ds Max等工具制作的三維模型,并添加到場景中。
本章以3ds Max為例,介紹如何導入外部模型。
10.1 支持格式Three.js有一系列導入外部文件的輔助函數(shù),是在three.js之外的,使用前需要額外下載,在https://github.com/mrdoob/three.js/tree/master/examples/js/loaders可以找到,選擇對應的模型加載器,系在下來。
*.obj是最常用的模型格式,導入*.obj文件需要OBJLoader.js;導入帶*.mtl材質(zhì)的*.obj文件需要MTLLoader.js以及OBJMTLLoader.js。另有PLYLoader.js、STLLoader.js等分別對應不同格式的加載器,可以根據(jù)模型格式自行選擇。
目前,支持的模型格式有:
*.obj
*.obj, *.mtl
*.dae
*.ctm
*.ply
*.stl
*.wrl
*.vtk
10.2 無材質(zhì)的模型本節(jié)中,我們將將導出的沒有材質(zhì)的模型使用Three.js導入場景中。
首先,下載OBJLoader.js并在HTML的
中使用:然后,我們需要準備一個*.obj模型,在init函數(shù)中,創(chuàng)建loader變量,用于導入模型:
var loader = new THREE.OBJLoader();
loader導入模型的時候,接受兩個參數(shù),第一個表示模型路徑,第二個表示完成導入后的回調(diào)函數(shù),一般我們需要在這個回調(diào)函數(shù)中將導入的模型添加到場景中。
loader.load("../lib/port.obj", function(obj) { //儲存到全局變量中 mesh = obj; scene.add(obj); });
可以看到一個沒有材質(zhì)的茶壺
我們在重繪函數(shù)中讓茶壺旋轉(zhuǎn):
function draw() { renderer.render(scene, camera); mesh.rotation.y += 0.01; if (mesh.rotation.y > Math.PI * 2) { mesh.rotation.y -= Math.PI * 2; } }
可以看到在某些角度時,好像有些面片沒有被繪制出來,因而后方的茶嘴似乎穿越到前方了:
這是由于默認的情況下,只有正面的面片被繪制,而如果需要雙面繪制,需要這樣設置:
var loader = new THREE.OBJLoader(); loader.load("port.obj", function(obj) { obj.traverse(function(child) { if (child instanceof THREE.Mesh) { child.material.side = THREE.DoubleSide; } }); mesh = obj; scene.add(obj); });
完整代碼:
效果圖:
10.3 有材質(zhì)的模型10.3.1 代碼中設置材質(zhì)模型的材質(zhì)可以有兩種定義方式,一種是在代碼中導入模型后設置材質(zhì),另一種是在建模軟件中導出材質(zhì)信息。下面,我們將分別介紹這兩種方法。
這種方法與上一節(jié)類似,不同之處在于回調(diào)函數(shù)中設置模型的材質(zhì):
var loader = new THREE.OBJLoader(); loader.load("port.obj", function(obj) { obj.traverse(function(child) { if (child instanceof THREE.Mesh) { /* 修改這里以下的代碼 */ child.material = new THREE.MeshLambertMaterial({ color: 0xffff00, side: THREE.DoubleSide }); /* 修改這里以上的代碼 */ } }); mesh = obj; scene.add(obj); });
效果圖:
10.3.2 建模軟件中設置材質(zhì)導出3D模型的時候,選擇導出port.obj模型文件以及port.mtl材質(zhì)文件。
現(xiàn)在,我們不再使用OBJLoader.js,而是使用MTLLoader.js與OBJMTLLoader.js,并且要按該順序引用:
調(diào)用的方法也略有不同:
var mtlLoader = new THREE.MTLLoader(); mtlLoader.setPath(""); mtlLoader.load("port.mtl", function(materials) { materials.preload(); // model loader var objLoader = new THREE.OBJLoader(); objLoader.setMaterials(materials); objLoader.setPath(""); objLoader.load("port.obj", function(object) { object.position.y = -95; // if has object, add to scene if (object.children.length > 0) { scene.add(object.children[0]); } }); });
完整代碼:
導出時自帶的效果圖:
11. 光與影圖像渲染的豐富效果很大程度上也要歸功于光與影的利用。真實世界中的光影效果非常復雜,但是其本質(zhì)—光的傳播原理卻又是非常單一的,這便是自然界繁簡相成的又一例證。為了使計算機模擬豐富的光照效果,人們提出了幾種不同的光源模型(環(huán)境光、平行光、點光源、聚光燈等),在不同場合下組合利用,將能達到很好的光照效果。
在Three.js中,光源與陰影的創(chuàng)建和使用是十分方便的。在學會了如何控制光影的基本方法之后,如果能將其靈活應用,將能使場景的渲染效果更加豐富逼真。在本章中,我們將探討四種常用的光源(環(huán)境光、點光源、平行光、聚光燈)和陰影帶來的效果,以及如何去創(chuàng)建使用光影。
11.1 環(huán)境光(AmbientLight)環(huán)境光是指場景整體的光照效果,是由于場景內(nèi)若干光源的多次反射形成的亮度一致的效果,通常用來為整個場景指定一個基礎(chǔ)亮度。因此,環(huán)境光沒有明確的光源位置,在各處形成的亮度也是一致的。
在設置環(huán)境光時,只需指定光的顏色:
var light = new THREE.AmbientLight(hex); scene.add(light);
其中hex是十六進制的RGB顏色信息,如紅色表示為0xff0000
但是,如果此時場景中沒有物體,只添加了這個環(huán)境光,那么渲染的結(jié)果仍然是一片黑。所以,我們添加兩個長方體看下效果:
// 創(chuàng)建一個綠色的正方體 var greenCube = new THREE.Mesh(new THREE.CubeGeometry(2, 2, 2), new THREE.MeshLambertMaterial({color: 0x00ff00})); greenCube.position.x = 3; scene.add(greenCube); // 創(chuàng)建一個白色的正方體 var whiteCube = new THREE.Mesh(new THREE.CubeGeometry(2, 2, 2), new THREE.MeshLambertMaterial({color: 0xffffff})); whiteCube.position.x = -3; scene.add(whiteCube);
效果如圖:
如果想讓環(huán)境光暗些,可以將其設置為new THREE.AmbientLight(0xcccccc)等,效果為:
11.2 點光源(PointLight)點光源是不計光源大小,可以看作一個點發(fā)出的光源。點光源照到不同物體表面的亮度是線性遞減的,因此,離點光源距離越遠的物體會顯得越暗。
點光源的構(gòu)造函數(shù)是:
THREE.PointLight(hex, intensity, distance);
其中,hex是光源十六進制的顏色值;intensity是亮度,缺省值為1,表示100%亮度;distance是光源最遠照射到的距離,缺省值為0。
創(chuàng)建點光源并將其添加到場景中的完整做法是:
var light = new THREE.PointLight(0xffffff, 2, 100); light.position.set(0, 1.5, 2); scene.add(light);
效果圖:
注意,這里光在每個面上的亮度是不同的,對于每個三角面片,將根據(jù)三個頂點的亮度進行插值。
11.3 平行光(DirectionalLight)我們都知道,太陽光常常被看作平行光,這是因為相對地球上物體的尺度而言,太陽離我們的距離足夠遠。對于任意平行的平面,平行光照射的亮度都是相同的,而與平面所在位置無關(guān)。
平行光的構(gòu)造函數(shù)是:
THREE.DirectionalLight(hex, intensity)
其中,hex是光源十六進制的顏色值;intensity是亮度,缺省值為1,表示100%亮度。
此外,對于平行光而言,設置光源位置尤為重要。
var light = new THREE.DirectionalLight(); light.position.set(2, 5, 3); scene.add(light);
注意,這里設置光源位置并不意味著所有光從(2, 5, 3)點射出(如果是的話,就成了點光源),而是意味著,平行光將以矢量(-2, -5, -3)的方向照射到所有平面。因此,平面亮度與平面的位置無關(guān),而只與平面的法向量相關(guān)。只要平面是平行的,那么得到的光照也一定是相同的。
示例代碼:
效果圖:
11.4 聚光燈(SpotLight)可以看出,聚光燈是一種特殊的點光源,它能夠朝著一個方向投射光線。聚光燈投射出的是類似圓錐形的光線,這與我們現(xiàn)實中看到的聚光燈是一致的。
其構(gòu)造函數(shù)為:
THREE.SpotLight(hex, intensity, distance, angle, exponent)
相比點光源,多了angle和exponent兩個參數(shù)。angle是聚光燈的張角,缺省值是Math.PI / 3,最大值是Math.PI / 2;exponent是光強在偏離target的衰減指數(shù)(target需要在之后定義,缺省值為(0, 0, 0)),缺省值是10。
在調(diào)用構(gòu)造函數(shù)之后,除了設置光源本身的位置,一般還需要設置target:
light.position.set(x1, y1, z1); light.target.position.set(x2, y2, z2);
除了設置light.target.position的方法外,如果想讓聚光燈跟著某一物體移動(就像真的聚光燈!),可以target指定為該物體:
var cube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1), new THREE.MeshLambertMaterial({color: 0x00ff00})); var light = new THREE.SpotLight(0xffff00, 1, 100, Math.PI / 6, 25); light.target = cube;
示例代碼:
效果圖:
11.5 陰影明暗是相對的,陰影的形成也就是因為比周圍獲得的光照更少。因此,要形成陰影,光源必不可少。
在Three.js中,能形成陰影的光源只有THREE.DirectionalLight與THREE.SpotLight;而相對地,能表現(xiàn)陰影效果的材質(zhì)只有THREE.LambertMaterial與THREE.PhongMaterial。因而在設置光源和材質(zhì)的時候,一定要注意這一點。
下面,我們以聚光燈為例,在之前的基礎(chǔ)上增加陰影效果。
首先,我們需要在初始化時,告訴渲染器渲染陰影:
renderer.shadowMapEnabled = true;
然后,對于光源以及所有要產(chǎn)生陰影的物體調(diào)用:
// 上面的案例,產(chǎn)生陰影的物體是正方體 cube.castShadow = true;
對于接收陰影的物體調(diào)用:
// 接收陰影的物體是平面 plan.receiveShadow = true;
比如場景中一個平面上有一個正方體,想要讓聚光燈照射在正方體上,產(chǎn)生的陰影投射在平面上,那么就需要對聚光燈和正方體調(diào)用castShadow = true,對于平面調(diào)用receiveShadow = true。
以上就是產(chǎn)生陰影效果的必要步驟了,不過通常還需要設置光源的陰影相關(guān)屬性,才能正確顯示出陰影效果。
對于聚光燈,需要設置shadowCameraNear、shadowCameraFar、shadowCameraFov三個值,類比我們在第二章學到的透視投影照相機,只有介于shadowCameraNear與shadowCameraFar之間的物體將產(chǎn)生陰影,shadowCameraFov表示張角。
對于平行光,需要設置shadowCameraNear、shadowCameraFar、shadowCameraLeft、shadowCameraRight、shadowCameraTop以及shadowCameraBottom六個值,相當于正交投影照相機的六個面。同樣,只有在這六個面圍成的長方體內(nèi)的物體才會產(chǎn)生陰影效果。
為了看到陰影照相機的位置,通常可以在調(diào)試時開啟light.shadowCameraVisible = true。
如果想要修改陰影的深淺,可以通過設置shadowDarkness,該值的范圍是0到1,越小越淺。
另外,這里實現(xiàn)陰影效果的方法是Shadow Mapping,即陰影是作為渲染前計算好的貼圖貼上去的,因而會受到貼圖像素大小的限制。所以可以通過設置shadowMapWidth與shadowMapHeight值控制貼圖的大小,來改變陰影的精確度。
而如果想實現(xiàn)軟陰影的效果,可以通過renderer.shadowMapSoft = true;方便地實現(xiàn)。
完整代碼:
效果圖:
補充問題本地服務器
1、下載安裝node.js,因為node.js自帶npm
2、打開電腦命令行工具,輸入npm install -g live-server 全局安裝
3、在需要運行文件的文件夾下,按住shift鍵,點擊鼠標右鍵在此處打開命令窗口
4、輸入live-server回車
nmp官方說明
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/90110.html
摘要:個人前端文章整理從最開始萌生寫文章的想法,到著手開始寫,再到現(xiàn)在已經(jīng)一年的時間了,由于工作比較忙,更新緩慢,后面還是會繼更新,現(xiàn)將已經(jīng)寫好的文章整理一個目錄,方便更多的小伙伴去學習。 showImg(https://segmentfault.com/img/remote/1460000017490740?w=1920&h=1080); 個人前端文章整理 從最開始萌生寫文章的想法,到著手...
摘要:一般說來,對于制圖建模軟通常使正交投影,這樣不會因為投影而改變物體比例而對于其他大多數(shù)應用,通常使用透視投影,因為這更接近人眼的觀察效果。 showImg(https://segmentfault.com/img/remote/1460000012581680?w=1920&h=1080); 1. 概述 1.1 什么是WebGL? WebGL是在瀏覽器中實現(xiàn)三維效果的一套規(guī)范 想要使用...
閱讀 1879·2021-09-27 13:35
閱讀 3433·2019-08-30 14:16
閱讀 2489·2019-08-30 10:52
閱讀 868·2019-08-29 16:35
閱讀 1420·2019-08-29 15:22
閱讀 3647·2019-08-23 18:21
閱讀 3138·2019-08-23 18:00
閱讀 3127·2019-08-23 16:50