目錄javascript
事物是廣泛聯繫的。爲了達到更加真實的渲染效果,不少時候須要利用被渲染物體在其餘狀態下的中間渲染結果,處理到最終顯示的渲染場景中。這種中間渲染結果,就保存在幀緩衝區對象(framebuffer object,簡稱FBO)中,用來替代顏色緩衝區或深度緩存區。因爲其結果並不直接被顯示出來,因此這種技術也被稱爲離屏繪製(offscreen drawing)。html
在以前的教程實例中,地形的顏色信息都是來自於頂點緩衝區對象。而在這篇教程中,準備寫出這樣一個示例:分別在幀緩衝區和顏色緩衝區中繪製同一塊地形,顏色緩衝區的顏色信息不經過頂點緩衝區獲取而經過幀緩衝區獲取。這個簡單的示例並無具體的實際意義,可是能更好的理解FBO,FBO是後續更高級技術的基礎。java
示例的完整代碼太長,這裏就不放出來了,能夠在文章尾部提供的地址自行下載;這裏主要講解其中的關鍵部分。git
這裏定義了兩組着色器,一組是繪製在幀緩衝區的:github
// 頂點着色器程序-繪製到幀緩存 var FRAME_VSHADER_SOURCE = 'attribute vec4 a_Position;\n' + //位置 'attribute vec4 a_Color;\n' + //顏色 'uniform mat4 u_MvpMatrix;\n' + 'varying vec4 v_Color;\n' + 'void main() {\n' + ' gl_Position = u_MvpMatrix * a_Position;\n' + // 設置頂點座標 ' v_Color = a_Color;\n' + '}\n'; // 片元着色器程序-繪製到幀緩存 var FRAME_FSHADER_SOURCE = 'precision mediump float;\n' + 'varying vec4 v_Color;\n' + 'void main() {\n' + ' gl_FragColor = v_Color;\n' + //將深度保存在FBO中 '}\n';
能夠看到這段着色器程序與繪製在顏色緩衝區的着色器沒有區別。另一組是正常繪製在顏色緩衝區的:web
// 頂點着色器程序 var VSHADER_SOURCE = 'attribute vec4 a_Position;\n' + //位置 'attribute vec4 a_Color;\n' + //顏色 'attribute vec4 a_Normal;\n' + //法向量 'uniform mat4 u_MvpMatrix;\n' + 'varying vec4 v_PositionFromLight;\n' + 'void main() {\n' + ' gl_Position = u_MvpMatrix * a_Position;\n' + ' v_PositionFromLight = gl_Position;\n' + '}\n'; // 片元着色器程序 var FSHADER_SOURCE = '#ifdef GL_ES\n' + 'precision mediump float;\n' + '#endif\n' + 'uniform sampler2D u_Sampler;\n' + //顏色貼圖 'varying vec4 v_PositionFromLight;\n' + 'void main() {\n' + //獲取顏色貼圖中的值 ' vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;\n' + ' gl_FragColor = texture2D(u_Sampler, shadowCoord.xy);\n' + '}\n';
這裏能夠看到最終位置仍然來自頂點數組,顏色倒是從一個紋理對象插值出來的。這個紋理對象正是幀緩衝區中關聯的紋理對象,它是在幀緩衝對象繪製以後傳遞過來的。編程
注意這裏關於紋理座標的計算,在《WebGL簡易教程(五):圖形變換(模型、視圖、投影變換)》這篇教程中曾經提到過,在通過頂點着色器以後,頂點座標會歸一化到-1到1之間;而紋理座標是在0到1之間的,因此這裏須要座標變換一下。canvas
首先仍然是進行一些初始化操做。獲取上下文後建立着色器,並初始化幀緩衝對象(FBO):數組
// 獲取 <canvas> 元素 var canvas = document.getElementById('webgl'); // 獲取WebGL渲染上下文 var gl = getWebGLContext(canvas); if (!gl) { console.log('Failed to get the rendering context for WebGL'); return; } //初始化兩個着色器,drawProgram繪製到界面,frameProgram繪製到幀緩存 var drawProgram = createProgram(gl, VSHADER_SOURCE, FSHADER_SOURCE); var frameProgram = createProgram(gl, FRAME_VSHADER_SOURCE, FRAME_FSHADER_SOURCE); if (!drawProgram || !frameProgram) { console.log('Failed to intialize shaders.'); return; } //從着色器中獲取地址,保存到對應的變量中 GetProgramLocation(gl, drawProgram, frameProgram); // 初始化幀緩衝區對象 (FBO) var fbo = initFramebufferObject(gl); if (!fbo) { console.log('Failed to intialize the framebuffer object (FBO)'); return; } // 開啓深度測試 gl.enable(gl.DEPTH_TEST); // 指定清空<canvas>的顏色 gl.clearColor(0.0, 0.0, 0.0, 1.0); //清空顏色和深度緩衝區 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
這裏的函數GetProgramLocation是功能將從着色器獲取的數據地址保存起來,由於涉及到一些切換着色器再分配數據的操做,保存到變量中方便一些:緩存
//從着色器中獲取地址,保存到對應的變量中 function GetProgramLocation(gl, drawProgram, frameProgram) { // Get the storage location of attribute variables and uniform variables drawProgram.a_Position = gl.getAttribLocation(drawProgram, 'a_Position'); drawProgram.u_MvpMatrix = gl.getUniformLocation(drawProgram, 'u_MvpMatrix'); if (drawProgram.a_Position < 0 || !drawProgram.u_MvpMatrix) { console.log('Failed to get the storage location of a_Position, u_MvpMatrix'); //return; } frameProgram.a_Position = gl.getAttribLocation(frameProgram, 'a_Position'); frameProgram.a_Color = gl.getAttribLocation(frameProgram, 'a_Color'); frameProgram.u_MvpMatrix = gl.getUniformLocation(frameProgram, 'u_MvpMatrix'); if (frameProgram.a_Position < 0 || frameProgram.a_TexCoord < 0 || !frameProgram.u_MvpMatrix) { console.log('Failed to get the storage location of a_Position, a_Color, u_MvpMatrix'); //return; } }
在示例中實際進行了兩次繪製操做,分別在幀緩衝區和顏色緩衝區中繪製了一遍。所以,須要用到兩組不一樣的着色器。可是同一時間內只能用一組着色器進行繪製工做,這裏就涉及到一個着色器切換的問題。
在以前的例子當中,都是經過WebGL組件cuon-utils中的函數initShaders來初始化着色器。這個函數實際上包含了建立着色器程序功能函數createProgram(),以及設置當前着色器函數gl.useProgram():
function initShaders(gl, vshader, fshader) { var program = createProgram(gl, vshader, fshader); if (!program) { console.log('Failed to create program'); return false; } gl.useProgram(program); gl.program = program; return true; }
在程序初始化的時候只須要建立着色器函數createProgram()就能夠了,在須要傳輸數據和繪製的時候再去設置當前的着色器gl.useProgram()。
除此以外,頂點緩衝區的使用也有所改變。在以前的教程《WebGL簡易教程(三):繪製一個三角形(緩衝區對象)》中介紹過使用頂點緩衝區的五個步驟:
可是爲了節省空間,兩個不一樣的着色器是使用相同的頂點緩衝區數據,在須要的時候切換分配數據。所以這裏能夠將以上五步分紅兩個函數——在初始化的時候,進行1~3步:向頂點緩衝區寫入數據,留待繪製的時候分配使用:
//向頂點緩衝區寫入數據,留待之後分配 function initArrayBufferForLaterUse(gl, data, num, type) { // Create a buffer object var buffer = gl.createBuffer(); if (!buffer) { console.log('Failed to create the buffer object'); return null; } // Write date into the buffer object gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); // Store the necessary information to assign the object to the attribute variable later buffer.num = num; buffer.type = type; return buffer; }
在繪製時切換到對應的着色器,進行4~5步:分配緩衝區對象並開啓鏈接:
//分配緩衝區對象並開啓鏈接 function initAttributeVariable(gl, a_attribute, buffer) { gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0); gl.enableVertexAttribArray(a_attribute); }
固然,頂點數據索引也同時分配到頂點緩衝區,須要的時候綁定緩衝區對象便可:
//向頂點緩衝區寫入索引,留待之後分配 function initElementArrayBufferForLaterUse(gl, data, type) { // Create a buffer object var buffer = gl.createBuffer(); if (!buffer) { console.log('Failed to create the buffer object'); return null; } // Write date into the buffer object gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW); buffer.type = type; return buffer; }
幀緩衝區對象保存的是渲染的中間結果,所以分別存在三個關聯對象——顏色關聯對象(color attachment)、深度關聯對象(depth attachment)和模板關聯對象(stencil attachment),用來代替顏色緩衝區、深度緩衝區和模板緩衝區。關聯對象分爲兩種:紋理對象和渲染緩衝區對象(renderbuffer object)。通常來講,能夠定義一個紋理對象做爲幀緩衝區的的顏色關聯對象,定義一個渲染緩衝區對象做爲幀緩衝區的深度關聯對象,來實現離屏繪製。
在函數initFramebufferObject()中進行了幀緩衝區的初始化工做。具體來講, 幀緩衝區的具體設置過程能夠分爲以下8步:
經過gl.createFramebuffer()來建立初始化對象:
// 初始化幀緩衝區對象 (FBO) function initFramebufferObject(gl) { //... // 建立幀緩衝區對象 (FBO) framebuffer = gl.createFramebuffer(); if (!framebuffer) { console.log('Failed to create frame buffer object'); return error(); } //... }
在教程《WebGL簡易教程(十一):紋理》中就已經介紹過如何建立紋理對象並設置紋理對象的參數。這裏的建立過程也是同樣的;只是細節略有不一樣:
function initFramebufferObject(gl) { //... // 建立紋理對象並設置其尺寸和參數 texture = gl.createTexture(); // 建立紋理對象 if (!texture) { console.log('Failed to create texture object'); return error(); } gl.bindTexture(gl.TEXTURE_2D, texture); // Bind the object to target gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // 設置紋理參數 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); framebuffer.texture = texture; // 保存紋理對象 //... }
經過函數gl.createRenderbuffer()建立渲染緩衝區對象,這個渲染緩衝區對象將被指定成深度關聯對象。
function initFramebufferObject(gl) { //... // 建立渲染緩衝區對象並設置其尺寸和參數 depthBuffer = gl.createRenderbuffer(); //建立渲染緩衝區 if (!depthBuffer) { console.log('Failed to create renderbuffer object'); return error(); } //... }
將渲染緩衝區綁定到目標上,經過目標設置渲染緩衝區的尺寸等參數。
function initFramebufferObject(gl) { //... gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); // Bind the object to target gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT); //... }
對於WebGL/OpenGL而言,任何緩衝區對象都是須要綁定到目標上,再對目標進行操做的。綁定函數gl.bindRenderbuffer()的定義爲:
綁定完成後,經過gl.renderbufferStorage()函數設置渲染緩衝區的格式、寬度以及高度等。注意深度關聯的渲染緩衝區,其寬度和高度必須與做爲顏色關聯對象的紋理緩衝區一致。其函數定義爲:
仍然是先將幀緩衝綁定到目標上,使用函數gl.bindFramebuffer()進行綁定:
function initFramebufferObject(gl) { //... // 將紋理和渲染緩衝區對象關聯到幀緩衝區對象上 gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); //關聯顏色 //... }
注意這裏的attachment參數的取值gl.COLOR_ATTACHMENT0,WebGL和OpenGL有所不一樣,WebGL只容許一個顏色關聯對象而OpenGL容許多個。
使用gl.framebufferRenderbuffer()函數將渲染緩衝區對象關聯到幀緩衝區的深度關聯對象:
function initFramebufferObject(gl) { //... gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); //關聯深度 //... }
其函數定義以下:
配置幀緩衝區的過程很複雜,WebGL提供了檢查函數gl.checkFramebufferStatus():
function initFramebufferObject(gl) { //... // 檢查幀緩衝區是否被正確設置 var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (gl.FRAMEBUFFER_COMPLETE !== e) { console.log('Frame buffer object is incomplete: ' + e.toString()); return error(); } //... }
在須要在幀緩衝區繪製的時候調用綁定幀緩衝區對象,在須要在顏色緩衝區繪製的時候接觸綁定。能夠經過gl.bindFramebuffer()函數實現,具體可看下一節內容。
初始化準備工做完成後,接下來在加載數據的後進行圖形繪製操做,調用繪製函數DrawDEM():
demFile.addEventListener("change", function (event) { //... reader.onload = function () { if (reader.result) { //讀取 var terrain = new Terrain(); if (!readDEMFile(reader.result, terrain)) { console.log("文件格式有誤,不能讀取該文件!"); } //繪製 DrawDEM(gl, canvas, fbo, frameProgram, drawProgram, terrain); } }
readDEMFile()是讀取解析DEM文件的函數,並保存到自定義的Terrain對象中,經過這個Terrain對象,調用DrawDEM()進行繪製:
//繪製 function DrawDEM(gl, canvas, fbo, frameProgram, drawProgram, terrain) { // 設置頂點位置 var demBufferObject = initVertexBuffersForDrawDEM(gl, terrain); if (!demBufferObject) { console.log('Failed to set the positions of the vertices'); return; } //獲取光線:平行光 var lightDirection = getLight(); //預先給着色器傳遞一些不變的量 { //使用幀緩衝區着色器 gl.useProgram(frameProgram); //設置MVP矩陣 setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, frameProgram); //使用顏色緩衝區着色器 gl.useProgram(drawProgram); //設置MVP矩陣 setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, drawProgram); //將繪製在幀緩衝區的紋理傳遞給顏色緩衝區着色器的0號紋理單元 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, fbo.texture); gl.uniform1i(drawProgram.u_Sampler, 0); gl.useProgram(null); } //開始繪製 var tick = function () { //幀緩存繪製 gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); //將繪製目標切換爲幀緩衝區對象FBO gl.viewport(0, 0, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT); // 爲FBO設置一個視口 gl.clearColor(0.2, 0.2, 0.4, 1.0); // Set clear color (the color is slightly changed) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear FBO gl.useProgram(frameProgram); //準備生成紋理貼圖 //分配緩衝區對象並開啓鏈接 initAttributeVariable(gl, frameProgram.a_Position, demBufferObject.vertexBuffer); // 頂點座標 initAttributeVariable(gl, frameProgram.a_Color, demBufferObject.colorBuffer); // 顏色 //分配索引並繪製 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer); gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0); //顏色緩存繪製 gl.bindFramebuffer(gl.FRAMEBUFFER, null); //將繪製目標切換爲顏色緩衝區 gl.viewport(0, 0, canvas.width, canvas.height); // 設置視口爲當前畫布的大小 gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear the color buffer gl.useProgram(drawProgram); // 準備進行繪製 //分配緩衝區對象並開啓鏈接 initAttributeVariable(gl, drawProgram.a_Position, demBufferObject.vertexBuffer); // Vertex coordinat //分配索引並繪製 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer); gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0); window.requestAnimationFrame(tick, canvas); }; tick(); }
首先第一步仍然是初始化頂點緩衝區數組,可是與以前不一樣的是這個只傳輸頂點數據到頂點緩衝區,並不鏈接頂點着色器,由於兩組着色器是公用頂點數據的,因此須要在切換着色器的時候分配着色器並鏈接:
function initVertexBuffersForDrawDEM(gl, terrain) { //DEM的一個網格是由兩個三角形組成的 // 0------1 1 // | | // | | // col col------col+1 var col = terrain.col; var row = terrain.row; var indices = new Uint16Array((row - 1) * (col - 1) * 6); var ci = 0; for (var yi = 0; yi < row - 1; yi++) { //for (var yi = 0; yi < 10; yi++) { for (var xi = 0; xi < col - 1; xi++) { indices[ci * 6] = yi * col + xi; indices[ci * 6 + 1] = (yi + 1) * col + xi; indices[ci * 6 + 2] = yi * col + xi + 1; indices[ci * 6 + 3] = (yi + 1) * col + xi; indices[ci * 6 + 4] = (yi + 1) * col + xi + 1; indices[ci * 6 + 5] = yi * col + xi + 1; ci++; } } var dem = new Object(); // Create the "Object" object to return multiple objects. // Write vertex information to buffer object dem.vertexBuffer = initArrayBufferForLaterUse(gl, terrain.vertices, 3, gl.FLOAT); dem.colorBuffer = initArrayBufferForLaterUse(gl, terrain.colors, 3, gl.FLOAT); dem.normalBuffer = initArrayBufferForLaterUse(gl, terrain.normals, 3, gl.FLOAT); dem.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_SHORT); if (!dem.vertexBuffer || !dem.colorBuffer || !dem.indexBuffer || !dem.normalBuffer) { return null; } dem.numIndices = indices.length; // Unbind the buffer object gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); return dem; }
爲了知足交互需求,繪製函數仍然是經過刷新頁面函數requestAnimationFrame()實現的,有的數據是固定隨幀不變的,這樣的數據能夠提早傳輸好。固然,這些數據不包含共用的頂點緩衝區數據:
//獲取光線:平行光 var lightDirection = getLight(); //預先給着色器傳遞一些不變的量 { //使用幀緩衝區着色器 gl.useProgram(frameProgram); //設置MVP矩陣 setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, frameProgram); //使用顏色緩衝區着色器 gl.useProgram(drawProgram); //設置MVP矩陣 setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, drawProgram); //將繪製在幀緩衝區的紋理傳遞給顏色緩衝區着色器的0號紋理單元 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, fbo.texture); gl.uniform1i(drawProgram.u_Sampler, 0); gl.useProgram(null); }
注意這裏經過函數gl.useProgram()切換了着色器,而後再分別給着色器傳輸數據。在這個例子只是經過幀緩衝區作顏色中轉,因此幀緩衝區和顏色緩衝區繪製的MVP矩陣是相同且固定的,因此能夠提早傳輸好。而且,將幀緩衝區關聯着顏色關聯對象的紋理對象,分配給顏色緩衝區的片元着色器。
刷新頁面函數requestAnimationFrame()的回調函數tick()中進行繪製,頁面每隔一段時間就會調用這個繪製函數。
爲了聲明當前是繪製到幀緩存的,首先將要綁定幀緩衝區對象gl.bindFramebuffer()。而後調用gl.viewport()函數定義一個繪圖的視口:
接下來仍是經過gl.useProgram()切換到對應的着色器,分配並鏈接頂點緩衝區的頂點數據;最後調用gl.drawElements()進行繪製便可。
相關的代碼以下:
//開始繪製 var tick = function () { //幀緩存繪製 gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); //將繪製目標切換爲幀緩衝區對象FBO gl.viewport(0, 0, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT); // 爲FBO設置一個視口 gl.clearColor(0.2, 0.2, 0.4, 1.0); // Set clear color (the color is slightly changed) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear FBO gl.useProgram(frameProgram); //準備生成紋理貼圖 //分配緩衝區對象並開啓鏈接 initAttributeVariable(gl, frameProgram.a_Position, demBufferObject.vertexBuffer); // 頂點座標 initAttributeVariable(gl, frameProgram.a_Color, demBufferObject.colorBuffer); // 顏色 //分配索引並繪製 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer); gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0); //... window.requestAnimationFrame(tick, canvas); }; tick(); }
繪製到顏色緩衝區的步驟也是一致的,只不過在繪製以前須要調用gl.bindFramebuffer(gl.FRAMEBUFFER, null)解除幀緩衝區綁定,將繪製目標切換到當前的顏色緩衝區。固然,設置視口和切換着色器操做都是必須的。相關代碼以下:
//開始繪製 var tick = function () { //... //顏色緩存繪製 gl.bindFramebuffer(gl.FRAMEBUFFER, null); //將繪製目標切換爲顏色緩衝區 gl.viewport(0, 0, canvas.width, canvas.height); // 設置視口爲當前畫布的大小 gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear the color buffer gl.useProgram(drawProgram); // 準備進行繪製 //分配緩衝區對象並開啓鏈接 initAttributeVariable(gl, drawProgram.a_Position, demBufferObject.vertexBuffer); // Vertex coordinat //分配索引並繪製 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer); gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0); window.requestAnimationFrame(tick, canvas); }; tick(); }
最後運行的結果以下,顯示的是一個特定角度的地形:
跟以前教程相比,示例彷佛沒有特別的地方。這個示例的關鍵點在於這個渲染效果通過了幀緩衝區的中轉,給更深刻的技術作準備——好比,下一篇要論述的技術:陰影。
原本部分代碼和插圖來自《WebGL編程指南》,源代碼連接:地址 。會在此共享目錄中持續更新後續的內容。