前言
今年的首要研究對(duì)象OpenGL基本研究的差不多了,突發(fā)奇想,想用CoreGraphics根據(jù)OpenGL的渲染流水線,渲染出3D圖形來。折騰了2天,寫出了個(gè)demo,效果如下。
其實(shí)這種通過2D渲染引擎渲染3D的技術(shù)方案在Flash時(shí)代我就聽說了,但是當(dāng)時(shí)對(duì)于3D技術(shù)不是很了解,并沒有做深入研究。
原理
在OpenGL中,每個(gè)頂點(diǎn)通過Vertex Shader的處理,被處理成基本的繪制圖形,比如三角形,再通過Fragment Shader處理各個(gè)像素點(diǎn)的顏色。最終以透視的效果呈現(xiàn)在屏幕上(當(dāng)然如果你用了正交矩陣就沒透視啦)。根據(jù)上面的原理,我在渲染方案中定義了兩種基本圖形,線和多邊形。線由2個(gè)頂點(diǎn)組成,多邊形由3到多個(gè)頂點(diǎn)組成。通過MVP矩陣對(duì)頂點(diǎn)進(jìn)行變換,然后用CoreGraphics繪制頂點(diǎn)變換后的圖形。上圖中的正方體就是由6個(gè)四邊形組成,錐體則是4個(gè)三角形組成。
基本繪制圖形
每個(gè)基本繪制圖形都會(huì)實(shí)現(xiàn)下面的協(xié)議,material是圖形的樣式,包括顏色,線條粗細(xì)等,transform方法用來對(duì)組成圖形的頂點(diǎn)進(jìn)行變換,并返回變換后的圖形。sortZRef會(huì)返回圖形在z方向排序的參考值,這個(gè)主要用來彌補(bǔ)CoreGraphics中無法進(jìn)行Depth Test的缺陷。不過目前的參考值計(jì)算方案還是有問題的,僅僅是計(jì)算了所有頂點(diǎn)變換后z的平均值而已。
publicprotocol?HT3DElement?{
varmaterial:?HT3DMaterial?{getset}
func?transform(matrix:?GLKMatrix4)?->?Self
func?sortZRef()?->?Float
}
下面我們來看看圖形-線的實(shí)現(xiàn)。
publicvarstartPoint:?GLKVector3
publicvarendPoint:?GLKVector3
publicvarmaterial:?HT3DMaterial
publicfunc?transform(matrix:?GLKMatrix4)?->?HT3DLineElement?{
let?newStartPoint?=?matrix?*?GLKVector4.init(vector3:?startPoint,?w:1)
let?newEndPoint?=?matrix?*?GLKVector4.init(vector3:?endPoint,?w:1)
returnHT3DLineElement.init(startPoint:?(newStartPoint?/?newStartPoint.w).xyz,?endPoint:
(newEndPoint
/?newEndPoint.w).xyz,?material:?material)
}
publicfunc?sortZRef()?->?Float?{
return(startPoint.z?+?endPoint.z)?/2.0
}
在使用矩陣對(duì)頂點(diǎn)變換后,要重新把頂點(diǎn)變換到屏幕空間,所以將頂點(diǎn)除以它的w。xyz是自定義的擴(kuò)展,獲取4維向量的前3維。
1(newStartPoint?/?newStartPoint.w).xyz
sortZRef的實(shí)現(xiàn)正如上面所說,求z的平均值。
幾何體Geometry
Geometry由一組基本圖形組成,比如一個(gè)正方體。Geometry提供modelMatrix對(duì)這一組基本圖形進(jìn)行變換。它的存在讓我們可以為每一組基本圖形提供不同的模型變換。同時(shí)它也肩負(fù)著管理圖形材質(zhì)的任務(wù)。可以通過它的setMaterialForElementsInRange方法為每一個(gè)基本圖形設(shè)置不同的樣式。
publicvarelements:?[HT3DElement]?
publicvarmaterials:?[HT3DMaterial]?=?[]
publicvarmodelMatrix:?GLKMatrix4?=?GLKMatrix4Identity
publicvarmaterial:?HT3DMaterial??{
returnmaterials.first
}
publicinit(elements:?[HT3DElement],?material:?HT3DMaterial)?{
self.elements?=?elements
self.materials.append(material)
self.setMaterialForElementsInRange(range:?Range.init(uncheckedBounds:?(0,
elements.count?-1)),?materialIndex:0)
}
publicfunc?setMaterialForElementsInRange(range:?Range,?materialIndex:?Int)?{
iflet?material?=?materials[cycle:?materialIndex]?{
forindexinrange.lowerBound...range.upperBound?{
iflet?element?=?self.elements?[safe:?index]?{
varele?=?element
ele.material?=?material
self.elements?[index]?=?ele
}
}
}
}
CoreGraphics渲染
為了方便其他渲染器的實(shí)現(xiàn),我定義了渲染器的協(xié)議。渲染器的主要功能就是渲染基本圖形的集合。
protocol?HT3DRenderContext?{
func?render(elements:?[HT3DElement])
}
為了更加方便的調(diào)用渲染器代碼,為該協(xié)議編寫了下面的擴(kuò)展方法。
extension?HT3DRenderContext?{
publicfunc?render(vpMatrix:?GLKMatrix4,?geometries:?[HT3DGeometry])?{
varelements:?[HT3DElement]?=?[]
forgeometryingeometries?{
let?_?=?geometry.elements?.map?{
elements.append($0.transform(matrix:?vpMatrix?*?geometry.modelMatrix))
}
}
elements.sort?{?$0.sortZRef()?>?$1.sortZRef()?}
render(elements:?elements)
}
}
這樣就可以很方便的使用VP(ProjectionMatrix * ViewMatrix)和幾何體列表渲染了。在這個(gè)方法中,我們將基本圖形的頂點(diǎn)使用VP和所屬幾何體的ModelMatrix進(jìn)行變換,然后將這些基本圖形按照z軸排序,從而模擬DepthTest,最后調(diào)用渲染器的渲染方法。這個(gè)方法的具體實(shí)現(xiàn)取決于你用什么樣的渲染器。本文自然采用了CoreGraphics渲染器。渲染器代碼在HT3DCGRenderContext.swift中。主要就是線和多邊形兩種基本圖形的渲染,非常簡單的代碼。
func?renderElement(context:?CGContext,?element:?HT3DLineElement)?{
context.setStrokeColor(UIColor.fromVec3(glkVector3:?element.material.lineColor).cgColor)
context.setLineWidth(element.material.lineWidth)
context.beginPath()
context.move(to:?convertCoordFromGLToCG(element.startPoint.cgPoint()))
context.addLine(to:?convertCoordFromGLToCG(element.endPoint.cgPoint()))
context.strokePath()
}
func?renderElement(context:?CGContext,?element:?HT3DPolygonElement)?{
context.setFillColor(UIColor.fromVec3(glkVector3:?element.material.diffuse).cgColor)
context.setStrokeColor(UIColor.fromVec3(glkVector3:?element.material.lineColor).cgColor)
context.setLineWidth(element.material.lineWidth)
context.beginPath()
element.points.first.map?{?context.move(to:?convertCoordFromGLToCG($0.cgPoint()))?}
forindexin1..?CGPoint?{
return(from?*?(1,?-1)?+1.0)?*0.5*?(canvasSize.width,?canvasSize.height)
}
其中convertCoordFromGLToCG用于將OpenGL坐標(biāo)轉(zhuǎn)換成CoreGraphics中的坐標(biāo),如果你對(duì)OpenGL坐標(biāo)不了解,可以去看我的OpenGL系列教程。
如果你對(duì)本文的其他關(guān)于OpenGL的概念也不理解,也可以去教程里面找找答案,畢竟本文中很多OpenGL的概念我只是一筆帶過。
整合
目前這個(gè)方案還只是開始階段,還有很多優(yōu)化和不足的地方有待改進(jìn)。比如使用更加精準(zhǔn)的z軸排序算法,提供基本光照模型等等。