ThreeJs 封裝了 WebGL 進行渲染時所涉及到的相關概念,如光照,材質,紋理以及相機等。除此以外,其還抽象了場景(Scene)以及用於渲染的渲染器(WebGLRenderer)。這些相關概念都被封裝成了一個對象,那麼它們是如何協做的呢,關係又是如何呢?這篇文章主要就是來分析一下 ThreeJs 中核心中的核心,即場景,物體,光照,材質,紋理以及相機這些對象是如何渲染的。html
下面截取了一個渲染效果圖,看起來還不錯是否是。這是 ThreeJs 的官方 demo lights / spotlights 的渲染效果圖。這個 demo 中就基本涉及到了上面所提的核心對象,下面我將基於此 demo 來分析這些核心對象是如何被組織在一塊兒進行渲染的。web
Demo 有一點點長,對於不熟悉 ThreeJs 的人來講會有一點點難度,所以這裏主要分析了構建、初始化以及渲染 3 個部分來分別說明。canvas
// 構建渲染器 WebGLRenderer
var renderer = new THREE.WebGLRenderer();
// 設置顯示比例
renderer.setPixelRatio( window.devicePixelRatio );
// 構建一個透視投影的相機
var camera = new THREE.PerspectiveCamera( 35, window.innerWidth / window.innerHeight, 1, 2000 );
// 構建一個軌道控制器,主要就是經過鼠標來控制相機沿目標物體旋轉,從而達到像在旋轉場景同樣,能夠從各個不一樣角度觀察物體
var controls = new THREE.OrbitControls( camera, renderer.domElement );
// 構建場景
var scene = new THREE.Scene();
// 構建Phong網格材質MeshPhongMaterial,該材質能夠模擬具備鏡面高光的光澤表面,一個用於接收陰影的平面,一個用於場景中的物體 Box
var matFloor = new THREE.MeshPhongMaterial();
var matBox = new THREE.MeshPhongMaterial( { color: 0xaaaaaa } );
// 構建幾何體,一樣分別用於 平面 和 Box
var geoFloor = new THREE.PlaneBufferGeometry( 2000, 2000 );
var geoBox = new THREE.BoxBufferGeometry( 3, 1, 2 );
// 構建平面網格 mesh
var mshFloor = new THREE.Mesh( geoFloor, matFloor );
mshFloor.rotation.x = - Math.PI * 0.5;
// 構建 box 網格 mesh
var mshBox = new THREE.Mesh( geoBox, matBox );
// 構建環境光
var ambient = new THREE.AmbientLight( 0x111111 );
// 構建 3 個不一樣顏色的 聚光燈(SpotLight)
var spotLight1 = createSpotlight( 0xFF7F00 );
var spotLight2 = createSpotlight( 0x00FF7F );
var spotLight3 = createSpotlight( 0x7F00FF );
// 聲明用於描述聚光燈的 3 個不一樣光束幫助器
var lightHelper1, lightHelper2, lightHelper3;
複製代碼
上面代碼中,基本上每一行都添加了詳細的註釋,其中有調用了一個內部的函數 createSpotlight() ,以下。數組
function createSpotlight( color ) {
var newObj = new THREE.SpotLight( color, 2 );
newObj.castShadow = true;
newObj.angle = 0.3;
newObj.penumbra = 0.2;
newObj.decay = 2;
newObj.distance = 50;
newObj.shadow.mapSize.width = 1024;
newObj.shadow.mapSize.height = 1024;
return newObj;
}
複製代碼
這個方法,主要就是根據指定的顏色構建一個聚光燈並設置好相應的參數。這裏不論是相機、光照、材質仍是物體,其詳細的參數並不打算在這裏一一講述,有須要的話再進一步說明。bash
function init() {
......
// 將平面,box,環境光以及光源輔助器等所有添加到 scene 中
scene.add( mshFloor );
scene.add( mshBox );
scene.add( ambient );
scene.add( spotLight1, spotLight2, spotLight3 );
scene.add( lightHelper1, lightHelper2, lightHelper3 );
document.body.appendChild( renderer.domElement );
onResize();
window.addEventListener( 'resize', onResize, false );
controls.target.set( 0, 7, 0 );
controls.maxPolarAngle = Math.PI / 2;
controls.update();
}
複製代碼
初始化主要就是將平面,box ,光照這些都添加進場景中,可是要注意,相機並無被添加進來。app
function render() {
TWEEN.update();
if ( lightHelper1 ) lightHelper1.update();
if ( lightHelper2 ) lightHelper2.update();
if ( lightHelper3 ) lightHelper3.update();
renderer.render( scene, camera );
requestAnimationFrame( render );
}
複製代碼
渲染函數 render() 中最關鍵的調用渲染器的 WebGLRenderer#render() 方法同時去渲染場景和相機。dom
根據上面的分析,以及對 ThreeJs 源碼的分析,梳理出以下 2 個類圖關係。 ide
圖中,渲染器負責同時渲染場景以及相機。而光照和網格都被添加到場景中。幾何體以及材質都是網格的 2 個基本屬性,也決定一個網格的形狀和表面紋理。函數
該圖是對上圖的補充,說明光照,相機以及網格都屬於 Object3D 對象。在 ThreeJs 中還有許多的類都是繼承自 Object3D 的。工具
先來看一下 WebGL 的流水線渲染管線圖,以下所示。這個是必需要了解的,咱們能夠沒必要徹底理解渲染管線的每一個步驟,但咱們必需要知道渲染管線的這個流程。
渲染管線指的是WebGL程序的執行過程,如上圖所示,主要分爲 4 個步驟:
頂點着色器的處理,主要是一組矩陣變換操做,用來把3D模型(頂點和原型)投影到viewport上,輸出是一個個的多邊形,好比三角形。
光柵化,也就是把三角形鏈接區域按必定的粒度逐行轉化成片元(fragement),相似於2D空間中,能夠把這些片元看作是3D空間的一個像素點。
片元着色器的處理,爲每一個片元添加顏色或者紋理。只要給出紋理或者顏色,以及紋理座標(uv),管線就會根據紋理座標進行插值運算,將紋理或者圖片着色在相應的片元上。
把3D空間的片元合併輸出爲2D像素數組並顯示在屏幕上。
由於做者也沒進行過原生的 WebGL 開發,而是一上來就擼起了 ThreeJs。因此 這裏僅根據 Open GL ES 的開發流程,繪製出以下流程圖。
流程圖中關鍵的第一步在於建立着色器(Shader)程序,着色器程序主要用 GLSL(GL Shading Language) 語言編寫,其直接由 GPU 來執行。第二步是設置頂點,紋理以及其餘屬性,如咱們建立的幾何圖元 Box,加載的 obj 文件,以及用於矩陣變換的模型矩陣,視圖矩陣以及投影矩陣等。第三步即是進行頂點的繪製,如以點繪製,以直線繪製以及以三角形繪製,對於圖元,大部分是以三角形繪製。
關於座標系與矩陣變換,這裏一個幅圖總結的很不錯,畫的很詳細,一眼就能看出其中的意思。
關於 WebGL 的基本就介紹這麼多,這裏的目的是爲了讓後面的分析有個簡單的鋪墊。若是感興趣,能夠參考更多大牛專門介紹 WebGL / Open GL ES 的文章。
WebGLRenderer 的初始化主要在它的構造方法 WebGLRenderer() 和 initGLContext() 中。這裏先看看構造方法 WebGLRenderer() 。
####2.1 構造方法 WebGLRenderer() 其初始化的屬性不少。這裏主要關注其 2 個最核心的屬性 canvas 以及 context。
function WebGLRenderer( parameters ) {
console.log( 'THREE.WebGLRenderer', REVISION );
parameters = parameters || {};
// 若是參數中有 canvas,就有參數中的,若是沒有就經過 document.createElementNS() 來建立一個。和 2D 的概念同樣,這裏的 canvas 主要是用來進行 3D 渲染的畫布。
var _canvas = parameters.canvas !== undefined ? parameters.canvas : document.createElementNS( 'http://www.w3.org/1999/xhtml', 'canvas' ),
_context = parameters.context !== undefined ? parameters.context : null,
......
// initialize
var _gl;
......
// 從 canvas 中獲取 context。參數 webgl 是其中之一,其還能夠獲取 2d 的。這裏獲取到 webgl 的 context,那就意味者能夠經過它進行 3D 繪製了。
_gl = _context || _canvas.getContext( 'webgl', contextAttributes ) || _canvas.getContext( 'experimental-webgl', contextAttributes );
......
function initGLContext() {
......
_this.context = _gl;
......
}
......
}
複製代碼
如上面的代碼以及註釋,canvas 就是 html 的標準元素
####2.2 初始化上下文方法 initGLContext()
function initGLContext() {
/**
* 擴展特性
*/
extensions = new WebGLExtensions( _gl );
capabilities = new WebGLCapabilities( _gl, extensions, parameters );
if ( ! capabilities.isWebGL2 ) {
extensions.get( 'WEBGL_depth_texture' );
extensions.get( 'OES_texture_float' );
extensions.get( 'OES_texture_half_float' );
extensions.get( 'OES_texture_half_float_linear' );
extensions.get( 'OES_standard_derivatives' );
extensions.get( 'OES_element_index_uint' );
extensions.get( 'ANGLE_instanced_arrays' );
}
extensions.get( 'OES_texture_float_linear' );
/**
* 工具類
*/
utils = new WebGLUtils( _gl, extensions, capabilities );
/**
* 狀態
*/
state = new WebGLState( _gl, extensions, utils, capabilities );
state.scissor( _currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ) );
state.viewport( _currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ) );
info = new WebGLInfo( _gl );
properties = new WebGLProperties();
/**
* 紋理輔助類
*/
textures = new WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info );
/**
* 屬性存儲輔助類,主要實現 JavaScript 中的變量或者數組、紋理圖片傳遞到 WebGL 中
*/
attributes = new WebGLAttributes( _gl );
/**
* 幾何圖元
*/
geometries = new WebGLGeometries( _gl, attributes, info );
/**
* Object 類存儲類
*/
objects = new WebGLObjects( geometries, info );
morphtargets = new WebGLMorphtargets( _gl );
/**
* WebGL program
*/
programCache = new WebGLPrograms( _this, extensions, capabilities );
renderLists = new WebGLRenderLists();
renderStates = new WebGLRenderStates();
/**
* 背景
*/
background = new WebGLBackground( _this, state, objects, _premultipliedAlpha );
/**
* Buffer
*/
bufferRenderer = new WebGLBufferRenderer( _gl, extensions, info, capabilities );
indexedBufferRenderer = new WebGLIndexedBufferRenderer( _gl, extensions, info, capabilities );
info.programs = programCache.programs;
_this.context = _gl;
_this.capabilities = capabilities;
_this.extensions = extensions;
_this.properties = properties;
_this.renderLists = renderLists;
_this.state = state;
_this.info = info;
}
複製代碼
initGLContext() 方法中初始化了不少的組件,有的組件很容易就能看出來是做什麼用的,而有的組件可能就沒那麼好知道意思,須要等到具體分析 render() 方法時,用到時再來理解。不過,雖然 initGLContext() 方法中看起來有不少的組件初始化,但實質這些組件也只是進行一個最基本的構造而已,沒有進一步更深刻的過程。所以,這裏也粗略的看一下便可。
整個 render 的過程是十分複雜的,也是漫長的,須要咱們耐心去看,去理解。先來簡單過一下它的時序圖。
從時序圖可見,其涉及到的相對象以及步驟是比較多的,共 20 步。其中涉及到的主要對象有:Scene,Camera,WebGLRenderStates,WebGLRenderLists,WebGLBackground,WebGLProgram,_gl,WebGLBufferRenderer。咱們比較熟悉的是 Scene,由於咱們的Object / Mesh 都是被添加到它裏面的,另外還有 Camera,咱們必需要有一個相機來告訴咱們以怎麼樣的視角來觀看這個 3D 世界。另一些不熟悉的對象,WebGLRenderList 管理着咱們須要拿去 render 的 Object / Mesh,WebGLBackground 描述了場景的背景,WebGLProgram 則建立了用於連接、執行 Shader 的程序,而 WebGLBufferRenderer 則是整個 3D 世界被 render 到的目的地。 這裏不會按照時序圖,逐步逐步地進行分析,而是挑重點,同時保持與前面所述的 OpenGL ES 的流程一致性上進行分析。
render() 函數
this.render = function ( scene, camera, renderTarget, forceClear ) {
// 前面是一些參數的校驗,這裏省略
// 1.reset caching for this frame
......
// 2.update scene graph
if ( scene.autoUpdate === true ) scene.updateMatrixWorld();
// 3.update camera matrices and frustum
if ( camera.parent === null ) camera.updateMatrixWorld();
.....
// 4. init WebGLRenderState
currentRenderState = renderStates.get( scene, camera );
currentRenderState.init();
scene.onBeforeRender( _this, scene, camera, renderTarget );
// 5.視景體矩陣計算,爲相機的投影矩陣與相機的世界矩陣的逆矩陣的叉乘?
_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
_frustum.setFromMatrix( _projScreenMatrix );
_localClippingEnabled = this.localClippingEnabled;
_clippingEnabled = _clipping.init( this.clippingPlanes, _localClippingEnabled, camera );
// 6.WebGLRenderList 的初始化
currentRenderList = renderLists.get( scene, camera );
currentRenderList.init();
projectObject( scene, camera, _this.sortObjects );
......
// 7. shadow 的繪製
if ( _clippingEnabled ) _clipping.beginShadows();
var shadowsArray = currentRenderState.state.shadowsArray;
shadowMap.render( shadowsArray, scene, camera );
currentRenderState.setupLights( camera );
if ( _clippingEnabled ) _clipping.endShadows();
//
if ( this.info.autoReset ) this.info.reset();
if ( renderTarget === undefined ) {
renderTarget = null;
}
this.setRenderTarget( renderTarget );
// 8.背景的繪製
background.render( currentRenderList, scene, camera, forceClear );
// 9.render scene
var opaqueObjects = currentRenderList.opaque;
var transparentObjects = currentRenderList.transparent;
if ( scene.overrideMaterial ) {
// 10.強制使用場景的材質 overrideMaterial 來統一 render 物體。
var overrideMaterial = scene.overrideMaterial;
if ( opaqueObjects.length ) renderObjects( opaqueObjects, scene, camera, overrideMaterial );
if ( transparentObjects.length ) renderObjects( transparentObjects, scene, camera, overrideMaterial );
} else {
// 11.分別對 opaque 和 transparent 的物體進行 render
// opaque pass (front-to-back order)
if ( opaqueObjects.length ) renderObjects( opaqueObjects, scene, camera );
// transparent pass (back-to-front order)
if ( transparentObjects.length ) renderObjects( transparentObjects, scene, camera );
}
// Generate mipmap if we're using any kind of mipmap filtering ..... // Ensure depth buffer writing is enabled so it can be cleared on next render state.buffers.depth.setTest( true ); state.buffers.depth.setMask( true ); state.buffers.color.setMask( true ); state.setPolygonOffset( false ); scene.onAfterRender( _this, scene, camera ); ...... currentRenderList = null; currentRenderState = null; }; 複製代碼
render() 是渲染的核心,粗略地看它作了大概如下的事情。
但這裏咱們沒必要關注每一個處理的細節,僅從幾個重要的點着手去理解以及分析。
即更新整個場景圖,主要就是更新每一個物體的 matrix。若是其含有孩子節點,則還會逐級更新。在這裏,每一個物體的 matrix 是經過其 position,quaternion以及scale 計算得來的,也就是其模型矩陣,而 matrixWorld 又是根據 matrix 計算得來的。若是當前節點沒有父節點,則 matrix 就是 matrixWorld。而若是有的話,那 matrixWorld 則爲父節點的 matrixWorld 與當前節點 matrix 的叉乘。也就是說當前節點的 matrixWorld 是相對於其父親節點的。
WebGLRenderList 的初始化init()方法自己並無什麼,其只是在 WebGLRenderLists 中經過將 scene.id 和 camera.id 創建起必定的關聯。而這裏更重要的目的是肯定有哪些對象是要被渲染出來的,這個最主要的實現就在 projectObject() 方法中。
function projectObject( object, camera, sortObjects ) {
if ( object.visible === false ) return;
var visible = object.layers.test( camera.layers );
if ( visible ) {
// 是否爲光照
if ( object.isLight ) {
currentRenderState.pushLight( object );
......
} else if ( object.isSprite ) {
// 是否爲精靈
if ( ! object.frustumCulled || _frustum.intersectsSprite( object ) ) {
......
currentRenderList.push( object, geometry, material, _vector3.z, null );
}
} else if ( object.isImmediateRenderObject ) {
// 是否爲當即要渲染的 Object
......
currentRenderList.push( object, null, object.material, _vector3.z, null );
} else if ( object.isMesh || object.isLine || object.isPoints ) {
// 是否爲 mesh,line,points
......
if ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) {
......
if ( Array.isArray( material ) ) {
var groups = geometry.groups;
for ( var i = 0, l = groups.length; i < l; i ++ ) {
......
if ( groupMaterial && groupMaterial.visible ) {
currentRenderList.push( object, geometry, groupMaterial, _vector3.z, group );
}
}
} else if ( material.visible ) {
// 可見便可渲染
currentRenderList.push( object, geometry, material, _vector3.z, null );
}
}
}
}
// 對每一個孩子進行遞歸遍歷
var children = object.children;
for ( var i = 0, l = children.length; i < l; i ++ ) {
projectObject( children[ i ], camera, sortObjects );
}
}
複製代碼
從方法中,咱們大體獲得以下結論:
經過 WebGLRenderList 的初始化基本就肯定了當前哪些 Object3D 對象是須要渲染的,接下來就是逐個 Object3D 的渲染了。
function renderObjects( renderList, scene, camera, overrideMaterial ) {
for ( var i = 0, l = renderList.length; i < l; i ++ ) {
var renderItem = renderList[ i ];
......
if ( camera.isArrayCamera ) {
......
} else {
_currentArrayCamera = null;
renderObject( object, scene, camera, geometry, material, group );
}
}
}
複製代碼
renderObjects 就是遍歷全部的 Object3D 對象,而後調用 renderObject() 方法進行進一步渲染。看來髒活都交給了 renderObject()。
function renderObject( object, scene, camera, geometry, material, group ) {
......
// 計算 mode view matrix 以及 normal matrix
object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
object.normalMatrix.getNormalMatrix( object.modelViewMatrix );
if ( object.isImmediateRenderObject ) {
......
} else {
_this.renderBufferDirect( camera, scene.fog, geometry, material, object, group );
}
......
}
複製代碼
關於計算 mode view matrix 以及 normal matrix,這裏我也不太看明白,因此我選擇先跳過。先分析後面的步驟。這裏不論是否 isImmediateRenderObject 其流程上差不太多,因此這裏先分析 renderBufferDirect()。
renderBufferDirect()方法
this.renderBufferDirect = function ( camera, fog, geometry, material, object, group ) {
......
// 1.經過WebGLState設置材質的一些屬性
state.setMaterial( material, frontFaceCW );
// 2.設置 program
var program = setProgram( camera, fog, material, object );
......
if ( updateBuffers ) {
// 3.設置頂點屬性
setupVertexAttributes( material, program, geometry );
if ( index !== null ) {
// 4.綁定 buffer
_gl.bindBuffer( _gl.ELEMENT_ARRAY_BUFFER, attribute.buffer );
}
}
......
// 5.根據不一樣網格類型肯定相應的繪製模式
if ( object.isMesh ) {
if ( material.wireframe === true ) {
......
renderer.setMode( _gl.LINES );
} else {
switch ( object.drawMode ) {
case TrianglesDrawMode:
renderer.setMode( _gl.TRIANGLES );
break;
case TriangleStripDrawMode:
renderer.setMode( _gl.TRIANGLE_STRIP );
break;
case TriangleFanDrawMode:
renderer.setMode( _gl.TRIANGLE_FAN );
break;
}
}
} else if ( object.isLine ) {
......
if ( object.isLineSegments ) {
renderer.setMode( _gl.LINES );
} else if ( object.isLineLoop ) {
renderer.setMode( _gl.LINE_LOOP );
} else {
renderer.setMode( _gl.LINE_STRIP );
}
} else if ( object.isPoints ) {
renderer.setMode( _gl.POINTS );
} else if ( object.isSprite ) {
renderer.setMode( _gl.TRIANGLES );
}
if ( geometry && geometry.isInstancedBufferGeometry ) {
if ( geometry.maxInstancedCount > 0 ) {
renderer.renderInstances( geometry, drawStart, drawCount );
}
} else {
// 6.調用 WebGLBufferRenderer#render() 方法進行渲染
renderer.render( drawStart, drawCount );
}
};
複製代碼
renderBufferDirect()方法是一個比較重要的方法,在這裏能夠看到一個物體被渲染的「最小完整流程」。
function setProgram( camera, fog, material, object ) {
.....
.....
var materialProperties = properties.get( material );
var lights = currentRenderState.state.lights;
if ( material.needsUpdate ) {
initMaterial( material, fog, object );
material.needsUpdate = false;
}
......
// 這裏的 program 即 WebGLProgram,也就是咱們在流程圖中所說的建立程序
var program = materialProperties.program,
p_uniforms = program.getUniforms(),
m_uniforms = materialProperties.shader.uniforms;
if ( state.useProgram( program.program ) ) {
refreshProgram = true;
refreshMaterial = true;
refreshLights = true;
}
......
p_uniforms.setValue( _gl, 'modelViewMatrix', object.modelViewMatrix );
p_uniforms.setValue( _gl, 'normalMatrix', object.normalMatrix );
p_uniforms.setValue( _gl, 'modelMatrix', object.matrixWorld );
return program;
}
複製代碼
這個方法自己是很長的,這裏省略了一萬字.... 咱們再來看看其主要所作的事情,這裏的 program 就是 WebGLProgram。而想知道 program 具體是什麼,這裏就涉及到了 WebGLProgram 的初始化。
function WebGLProgram( renderer, extensions, code, material, shader, parameters, capabilities ) {
var gl = renderer.context;
var defines = material.defines;
// 獲取頂點 shader 以及片元 shader
var vertexShader = shader.vertexShader;
var fragmentShader = shader.fragmentShader;
......
// 建立 program
var program = gl.createProgram();
......
// 構造最終用於進行渲染的 glsl,而且調用 WebGLShader 構造出 shader
var vertexGlsl = prefixVertex + vertexShader;
var fragmentGlsl = prefixFragment + fragmentShader;
// console.log( '*VERTEX*', vertexGlsl );
// console.log( '*FRAGMENT*', fragmentGlsl );
var glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
var glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );
// 將 program 關聯 shader
gl.attachShader( program, glVertexShader );
gl.attachShader( program, glFragmentShader );
......
// 連接 program
gl.linkProgram( program );
......
}
複製代碼
program 的初始化方法也是很是多的,這裏簡化出關鍵部分。再回憶一下前面的流程圖,就會明白這裏主要就是建立 program、shader ,關聯 program 和 shader,以及連接程序。連接好了程序以後接下來就能夠經過 useProgram() 使用 program 了,這一步驟在 setProgram() 中建立好 program 就調用了。 3. 設置頂點屬性,就是將咱們在外面所構造的 geometry 的頂點送到 shader 中去。 4. 綁定 buffer。 5. 根據不一樣網格類型肯定相應的繪製模式,如以 LINES 進行繪製,以TRIANGLES 進行繪製。 6. 調用 WebGLBufferRenderer#render() 方法進行渲染。以下,就是進行最後的 drawArrays() 調用,將上層建立的 geometry 以及 material(組合起來就叫作 mesh) 渲染到 3D 場景的 canvas 中。
function render( start, count ) {
gl.drawArrays( mode, start, count );
info.update( count, mode );
}
複製代碼
文章一樣以一篇 demo 爲入口對渲染過程進行了一個簡要的分析,其中還介紹了 OpenGL / WebGL 所須要知道的基礎知識。這其中瞭解了 OpenGL 的繪製流程以及各座標系之間的關係以及轉換,然後面的分析都是沿着這個繪製流程進行的。
然而,因爲做者的水平有限,而 OpenGL / WebGL 又是如此的強大,實在不能面面俱到,甚至對某些知識點也沒法透徹分析。所以,還請見諒。
最後,感謝你能讀到並讀完此文章,若是分析的過程當中存在錯誤或者疑問都歡迎留言討論。若是個人分享可以幫助到你,還請記得幫忙點個贊吧,謝謝。