2016-01-04 32 views
2

我需要渲染許多位於深層次結構中的小物體(大小爲2-100個三角形),每個物體都有自己的矩陣。爲了渲染它們,我預先計算每個對象的實際矩陣,將對象放在一個列表中,並且我有兩個調用來繪製每個對象:set matrix uniform和gl.drawElements()。OpenGL ES(WebGL)渲染許多小物體

顯然這不是最快的方法。然後我有幾千個物體的表現變得不可接受。我正在考慮的唯一解決方案是將多個對象分批存放到單個緩衝區中。但是做這件事並不容易,因爲每個對象都有自己的矩陣,並且要將對象放入共享緩衝區,所以我需要通過CPU上的矩陣來轉換它的頂點。更糟的問題是,用戶可以隨時移動任何對象,我需要重新計算大型頂點數據(因爲用戶可以移動多個嵌套子對象)

所以我正在尋找替代方法。而最近發現onshape.com項目奇怪的頂點着色器:

uniform mat4 uMVMatrix; 
uniform mat3 uNMatrix; 
uniform mat4 uPMatrix; 
  
uniform vec3 uSpecular; 
uniform float uOpacity; 
uniform float uColorAmbientFactor;  //Determines how much of the vertex-specified color to use in the ambient term 
uniform float uColorDiffuseFactor;  //Determines how much of the vertex-specified color to use in the diffuse term 
  
uniform bool uApplyTranslucentAlphaToAll; 
uniform float uTranslucentPassAlpha; 
  
attribute vec3 aVertexPosition; 
attribute vec3 aVertexNormal; 
attribute vec2 aTextureCoordinate; 
attribute vec4 aVertexColor; 
  
varying vec3 vPosition; 
varying lowp vec3 vNormal; 
varying mediump vec2 vTextureCoordinate; 
varying lowp vec3 vAmbient; 
varying lowp vec3 vDiffuse; 
varying lowp vec3 vSpecular; 
varying lowp float vOpacity; 
  
attribute vec4 aOccurrenceId; 
  
float unpackOccurrenceId() { 
  return aOccurrenceId.g * 65536.0 + aOccurrenceId.b * 256.0 + aOccurrenceId.a; 
} 
  
float unpackHashedBodyId() { 
  return aOccurrenceId.r; 
} 
  
#define USE_OCCURRENCE_TEXTURE 1 
  
#ifdef USE_OCCURRENCE_TEXTURE 
  
uniform sampler2D uOccurrenceDataTexture; 
uniform float uOccurrenceTexelWidth; 
uniform float uOccurrenceTexelHeight; 
#define ELEMENTS_PER_OCCURRENCE 2.0 
  
void getOccurrenceData(out vec4 occurrenceData[2]) { 
  // We will extract the occurrence data from the occurrence texture by converting the occurrence id to texture coordinates 
  
  // Convert the packed occurrenceId into a single number 
  float occurrenceId = unpackOccurrenceId(); 
  
  // We first determine the row of the texture by dividing by the overall texture width.  Each occurrence 
  // has multiple rgba texture entries, so we need to account for each of those entries when determining the 
  // element's offset into the buffer. 
  float divided = (ELEMENTS_PER_OCCURRENCE * occurrenceId) * uOccurrenceTexelWidth; 
  float row = floor(divided); 
  vec2 coordinate; 
  // The actual coordinate lies between 0 and 1.  We need to take care that coordinate lies on the texel 
  // center by offsetting the coordinate by a half texel. 
  coordinate.t = (0.5 + row) * uOccurrenceTexelHeight; 
  // Figure out the width of one texel in texture space 
  // Since we've already done the texture width division, we can figure out the horizontal coordinate 
  // by adding a half-texel width to the remainder 
  coordinate.s = (divided - row) + 0.5 * uOccurrenceTexelWidth; 
  occurrenceData[0] = texture2D(uOccurrenceDataTexture, coordinate); 
  // The second piece of texture data will lie in the adjacent column 
  coordinate.s += uOccurrenceTexelWidth; 
  occurrenceData[1] = texture2D(uOccurrenceDataTexture, coordinate); 
} 
  
#else 
  
attribute vec4 aOccurrenceData0; 
attribute vec4 aOccurrenceData1; 
void getOccurrenceData(out vec4 occurrenceData[2]) { 
  occurrenceData[0] = aOccurrenceData0; 
  occurrenceData[1] = aOccurrenceData1; 
} 
  
#endif 
  
/** 
 * Create a model matrix from the given occurrence data. 
 * 
 * The method for deriving the rotation matrix from the euler angles is based on this publication: 
 * http://www.soi.city.ac.uk/~sbbh653/publications/euler.pdf 
 */ 
mat4 createModelTransformationFromOccurrenceData(vec4 occurrenceData[2]) { 
  float cx = cos(occurrenceData[0].x); 
  float sx = sin(occurrenceData[0].x); 
  float cy = cos(occurrenceData[0].y); 
  float sy = sin(occurrenceData[0].y); 
  float cz = cos(occurrenceData[0].z); 
  float sz = sin(occurrenceData[0].z); 
  
  mat4 modelMatrix = mat4(1.0); 
  
  float scale = occurrenceData[0][3]; 
  
  modelMatrix[0][0] = (cy * cz) * scale; 
  modelMatrix[0][1] = (cy * sz) * scale; 
  modelMatrix[0][2] = -sy * scale; 
  
  modelMatrix[1][0] = (sx * sy * cz - cx * sz) * scale; 
  modelMatrix[1][1] = (sx * sy * sz + cx * cz) * scale; 
  modelMatrix[1][2] = (sx * cy) * scale; 
  
  modelMatrix[2][0] = (cx * sy * cz + sx * sz) * scale; 
  modelMatrix[2][1] = (cx * sy * sz - sx * cz) * scale; 
  modelMatrix[2][2] = (cx * cy) * scale; 
  
  modelMatrix[3].xyz = occurrenceData[1].xyz; 
  
  return modelMatrix; 
} 
  
  
void main(void) { 
  vec4 occurrenceData[2]; 
  getOccurrenceData(occurrenceData); 
  mat4 modelMatrix = createModelTransformationFromOccurrenceData(occurrenceData); 
  mat3 normalMatrix = mat3(modelMatrix); 
  
  vec4 position = uMVMatrix * modelMatrix * vec4(aVertexPosition, 1.0); 
  vPosition = position.xyz; 
  vNormal = uNMatrix * normalMatrix * aVertexNormal; 
  vTextureCoordinate = aTextureCoordinate; 
  
  vAmbient = uColorAmbientFactor * aVertexColor.rgb; 
  vDiffuse = uColorDiffuseFactor * aVertexColor.rgb; 
  vSpecular = uSpecular; 
  vOpacity = uApplyTranslucentAlphaToAll ? (min(uTranslucentPassAlpha, aVertexColor.a)) : aVertexColor.a; 
  
  gl_Position = uPMatrix * position; 
} 

看起來它們編碼對象的位置和旋轉角度爲2項中4組分浮紋,添加屬性存儲每個頂點的位置,在這個轉換紋理,然後在頂點着色器中執行矩陣計算。

所以問題是這個着色器實際上是對我的問題有效的解決方案,或者我應該更好地使用批處理或其他東西?

PS:可能更好的方法是存儲四元數而不是角度並直接轉換頂點?

回答

2

我也很好奇,所以我用4種不同的繪圖技術進行了幾次測試。

第一個是通過制服在大多數教程和書籍中找到的實例。對於每個模型,設置制服,然後繪製模型。

第二個是存儲一個額外的屬性,每個頂點的矩陣變換,並在GPU上進行變換。在每次繪製中,gl.bufferSubData然後在每個繪製中繪製儘可能多的模型。

第三種方法是將多個矩陣變換均勻地上傳到GPU,並在每個頂點上有一個額外的矩陣ID以在GPU上選擇正確的矩陣。這與第一個類似,只是它允許模型分批繪製。這也是它通常在骨架動畫中實現的方式。在繪製時間內,對於每批次,從批量[索引]處的模型上傳矩陣到GPU中的矩陣陣列[索引]並繪製批處理。

最後的技術是通過紋理查找。我創建了一個大小爲4096 * 256 * 4的Float32Array,其中包含每個模型的世界矩陣(足夠用於〜256k模型)。每個模型都有一個modelIndex屬性,用於從紋理中讀取其矩陣。然後在每一幀,gl.texSubImage2D整個紋理並在每次繪製調用中儘可能多地繪製。

沒有考慮硬件實例化,因爲我認爲要求繪製許多獨特的模型,即使對於我的測試,我只繪製每幀具有不同世界矩陣的立方體。

下面是結果:(多少能在60FPS繪製)每個模型

  1. 不同的制服:〜2000
  2. 成批制服matrixId:每個頂點〜20000
  3. 商店轉換:〜 40000(發現第一個實現中的錯誤)
  4. 紋理查找:〜160000
  5. 沒有平局,只是CPU時間來計算矩陣:〜170000

我認爲這很明顯,統一實例是不是走的路。技術1僅僅因爲它做了太多的平局而失敗。批量的制服應該處理繪製調用問題,但是我發現使用太多的CPU時間從正確的模型獲取矩陣數據並將其上傳到GPU。許多uniformMatrix4f調用也沒有幫助。

執行gl.texSubImage2D所花費的時間與計算動態對象的新世界矩陣所用的時間相比要少得多。在每個頂點上覆制變換數據的效果比大多數人想象的要好,但它浪費了大量的內存帶寬。紋理查找方法可能對以上所有技術中的CPU最爲友好。做4紋理查找的速度似乎與做統一數組查找相似。 (來自我對GPU進行綁定的較大複雜對象的測試結果)。

下面是使用紋理查找方法測試的一個快照: enter image description here

所以,綜上所述,你所追求的可能是要麼轉化數據存儲在每個頂點,如果你的模型是小或當模型很大時使用紋理查找方法。

答案在評論的問題:

  1. 填充率:我並沒有被束縛的GPU在所有。當我嘗試使用大型複雜模型時,統一實例成爲最快的。我想有一些使用統一批量和紋理查找的GPU開銷會導致它們變慢。
  2. 存儲四元數和翻譯:在我的情況下不會太重要,因爲正如你所看到的,texSubImage2D只佔用了9%的CPU時間,將其減少到4.5%並不重要。很難說它對GPU的影響,因爲雖然你的紋理查找較少,但你必須將四元數和平移轉換爲矩陣。
  3. 交錯:假設您的應用是頂點綁定的,這種技術可以使速度提高5-10%。然而,我從來沒有見過交錯在我的測試中對我有所幫助。所以我完全擺脫了它。
  4. 內存:對於除了每個頂點上的重複之外的所有技術基本相同。所有其他3種技術都應將相同數量的數據傳遞給GPU。 (你可以通過翻譯+四元數作爲統一而不是矩陣)
+1

非常感謝您的研究!如果您添加有關您的硬件的信息並且您的結果填充有限,那會更好嗎? PS:你有沒有試過存儲四元數和位置,導致只有2個紋理查找?PPS:您是否嘗試過交錯和非交錯屬性表現? – Rem

+0

請爲每種技術添加內存要求。新人很容易推斷哪一個可以選擇! – Rem

2

this這可能會給你一些想法。

如果瞭解雷姆的評論...

最簡單的解決方案是將某種每個頂點變換數據。這實際上就是上面的視頻。該解決方案的問題在於,如果您有一個包含100個頂點的模型,則必須更新所有100個頂點的變換。

解決方法是通過紋理間接轉換。對於每一個模型商店只是一個額外的浮動每個頂點,我們可以在

attribute float modelId; 

稱之爲浮動「modelId」因爲所以在第一個模型的所有頂點獲得ID = 0,所有頂點在第二個模型中獲取ID = 1等。

然後,您將變換存儲在紋理中。例如,您可能會存儲翻譯(x,y,z)+四元數(x,y,z,w)。如果您的目標平臺支持浮點紋理,那麼每個變換就是2個RGBA像素。

您可以使用modelId來計算紋理中將拉伸變換數據的位置。

float col = mod(modelId, halfTextureWidth) * 2.; 
float row = floor(modelId/halfTextureWidth); 
float oneHPixel = 1./textureWidth; 
vec2 uv = vec2((col + 0.5)/textureWidth, (row + 0.5)/textureHeight); 
vec4 translation = texture2D(transforms, uv); 
vec4 rotationQuat = texture2D(transform, uv + vec2(oneHPixel, 0)); 

現在,您可以使用translation和rotationQuat在頂點着色器中創建矩陣。

爲什麼halfTextureWidth?因爲我們每個變換都做2個像素。

爲什麼+ 0.5?請參閱https://stackoverflow.com/a/27439675/128511

這意味着您只需更新每個模型1個轉換,而不是每個頂點1個轉換,這使其成爲最小工作量。

This example generates some matrices from quaternions。這是一個類似的想法,但由於它在做粒子,它不需要紋理間接。

注意:以上假設您只需要翻譯和旋轉。沒有什麼能夠阻止你將整個矩陣存儲在紋理中,如果這是你需要的。或者像材料屬性,照明屬性等其他任何事情。

AFAIK幾乎所有當前平臺都支持從浮點紋理讀取數據。您必須啓用該功能

var ext = gl.getExtension("OES_texture_float"); 
if (!ext) { 
    // no floating point textures for you! 
} 

但請注意不是每個平臺都支持過濾浮點紋理。此解決方案不需要過濾(並且需要單獨啓用)。請務必將您的過濾設置爲gl.NEAREST

+0

無論如何經過測試,我發現甚至在JavaScript中轉換甚至50萬個三角形,將每幀畫成單個gl緩衝區,給了我一個體面的表現!因此,我最終在幾個批次(16k個三角形的大小)之間分割我的對象,並重新計算整個批次,如果其中的任何對象被更改或刪除 – Rem

+0

@gman既然你在這裏,你的想法是'texSubImage2D'和' uniformMatrix4fv'?因爲如果對象是半動態的,那麼我希望需要調用很多小的'texSubImage2D'或每幀替換整個紋理。我似乎記得,與桌面相比,webgl中的紋理數據更改具有顯着更大的開銷,這是真的嗎? –

+0

@Rem所以這個答案被接受,但爲你工作的解決方案是從我的。感覺受到傷害。除了JK之外,我希望對您在OP中發佈的技巧的這種極好的解釋清楚地表明,您不是每個對象更新1個浮動,而是翻譯+四元組中的8個浮動。偏移量不變,但您仍然需要更新偏移量指向的數據。 –