這篇教程的所有源代碼都可以在 GitHub 上找到。
介紹
OpenGL 是一門相當好的技術(shù),適用于從桌面的 GUI 到游戲,到移動應(yīng)用甚至 web 應(yīng)用的多種類型的繪圖工作。我敢保證,你今天看到的圖形有些就是用 OpenGL 渲染的??墒?,不管 OpenGL 多受歡迎、有多好用,與學習其它高級繪圖庫相比,學習 OpenGL 是要相當足夠的決心的。
這個教程的目的是給你一個切入點,讓你對 OpenGL 有個基本的了解,然后教你怎么用 Go 操作它。幾乎每種編程語言都有綁定 OpenGL 的庫,Go 也不例外,它有 go-gl 這個包。這是一個完整的套件,可以綁定 OpenGL ,適用于多種版本的 OpenGL。
這篇教程會按照下面列出的幾個階段進行介紹,我們最終的目標是用 OpenGL 在桌面窗口繪制游戲面板,進而實現(xiàn)康威生命游戲。完整的源代碼可以在 GitHub github.com/KyleBanks/conways-gol 上獲得,當你有疑惑的時候可以隨時查看源代碼,或者你要按照自己的方式學習也可以參考這個代碼。
在我們開始之前,我們要先弄明白康威生命游戲 到底是什么。這里是 Wikipedia 上面的總結(jié):
《生命游戲》,也可以簡稱為 Life,是一個細胞自動變化的過程,由英國數(shù)學家 John Horton Conway 于 1970 年提出。
這個“游戲”沒有玩家,也就是說它的發(fā)展依靠的是它的初始狀態(tài),不需要輸入。用戶通過創(chuàng)建初始配置文件、觀察它如何演變,或者對于高級“玩家”可以創(chuàng)建特殊屬性的模式,進而與《生命游戲》進行交互。
規(guī)則
《生命游戲》的世界是一個無窮多的二維正交的正方形細胞的格子世界,每一個格子都有兩種可能的狀態(tài),“存活”或者“死亡”,也可以說是“填充態(tài)”或“未填充態(tài)”(區(qū)別可能很小,可以把它看作一個模擬人類/哺乳動物行為的早期模型,這要看一個人是如何看待方格里的空白)。每一個細胞與它周圍的八個細胞相關(guān)聯(lián),這八個細胞分別是水平、垂直、斜對角相接的。在游戲中的每一步,下列事情中的一件將會發(fā)生:
- 當任何一個存活的細胞的附近少于 2 個存活的細胞時,該細胞將會消亡,就像人口過少所導(dǎo)致的結(jié)果一樣
- 當任何一個存活的細胞的附近有 2 至 3 個存活的細胞時,該細胞在下一代中仍然存活。
- 當任何一個存活的細胞的附近多于 3 個存活的細胞時,該細胞將會消亡,就像人口過多所導(dǎo)致的結(jié)果一樣
- 任何一個消亡的細胞附近剛好有 3 個存活的細胞,該細胞會變?yōu)榇婊畹臓顟B(tài),就像重生一樣。
不需要其他工具,這里有一個我們將會制作的演示程序:
Conway's Game of Life - 示例游戲
在我們的運行過程中,白色的細胞表示它是存活著的,黑色的細胞表示它已經(jīng)死亡。
概述
本教程將會涉及到很多基礎(chǔ)內(nèi)容,從最基本的開始,但是你還是要對 Go 由一些最基本的了解 —— 至少你應(yīng)該知道變量、切片、函數(shù)和結(jié)構(gòu)體,并且裝了一個 Go 的運行環(huán)境。我寫這篇教程用的 Go 版本是 1.8,但它應(yīng)該與之前的版本兼容。這里用 Go 語言實現(xiàn)沒有什么特別新奇的東西,因此只要你有過類似的編程經(jīng)歷就行。
這里是我們在這個教程里將會講到的東西:
最后的源代碼可以在 GitHub 上獲得,每一節(jié)的末尾有個回顧,包含該節(jié)相關(guān)的代碼。如果有什么不清楚的地方或者是你感到疑惑的,看看每一節(jié)末尾的完整代碼。
現(xiàn)在就開始吧!
安裝 OpenGL 和 GLFW
我們介紹過 OpenGL,但是為了使用它,我們要有個窗口可以繪制東西。 GLFW 是一款用于 OpenGL 的跨平臺 API,允許我們創(chuàng)建并使用窗口,而且它也是 go-gl 套件中提供的。
我們要做的第一件事就是確定 OpenGL 的版本。為了方便本教程,我們將會使用 OpenGL v4.1
,但要是你的操作系統(tǒng)不支持最新的 OpenGL,你也可以用 v2.1
。要安裝 OpenGL,我們需要做這些事:
# 對于 OpenGL 4.1
$ go get github.com/go-gl/gl/v4.1-core/gl
# 或者 2.1
$ go get github.com/go-gl/gl/v2.1/gl
然后是安裝 GLFW:
$ go get github.com/go-gl/glfw/v3.2/glfw
安裝好這兩個包之后,我們就可以開始了!先創(chuàng)建 main.go
文件,導(dǎo)入相應(yīng)的包(我們待會兒會用到的其它東西)。
package main
import (
"log"
"runtime"
"github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
"github.com/go-gl/glfw/v3.2/glfw"
)
接下來定義一個叫做 main
的函數(shù),這是用來初始化 OpenGL 以及 GLFW,并顯示窗口的:
const (
width = 500
height = 500
)
func main() {
runtime.LockOSThread()
window := initGlfw()
defer glfw.Terminate()
for !window.ShouldClose() {
// TODO
}
}
// initGlfw 初始化 glfw 并且返回一個可用的窗口。
func initGlfw() *glfw.Window {
if err := glfw.Init(); err != nil {
panic(err)
}
glfw.WindowHint(glfw.Resizable, glfw.False)
glfw.WindowHint(glfw.ContextVersionMajor, 4) // OR 2
glfw.WindowHint(glfw.ContextVersionMinor, 1)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
if err != nil {
panic(err)
}
window.MakeContextCurrent()
return window
}
好了,讓我們花一分鐘來運行一下這個程序,看看會發(fā)生什么。首先定義了一些常量, width
和 height
—— 它們決定窗口的像素大小。
然后就是 main
函數(shù)。這里我們使用了 runtime
包的 LockOSThread()
,這能確保我們總是在操作系統(tǒng)的同一個線程中運行代碼,這對 GLFW 來說很重要,GLFW 需要在其被初始化之后的線程里被調(diào)用。講完這個,接下來我們調(diào)用 initGlfw
來獲得一個窗口的引用,并且推遲(defer
)其終止。窗口的引用會被用在一個 for
循環(huán)中,只要窗口處于打開的狀態(tài),就執(zhí)行某些事情。我們待會兒會講要做的事情是什么。
initGlfw
是另一個函數(shù),這里我們調(diào)用 glfw.Init()
來初始化 GLFW 包。然后我們定義了 GLFW 的一些全局屬性,包括禁用調(diào)整窗口大小和改變 OpenGL 的屬性。然后創(chuàng)建了 glfw.Window
,這會在稍后的繪圖中用到。我們僅僅告訴它我們想要的寬度和高度,以及標題,然后調(diào)用 window.MakeContextCurrent
,將窗口綁定到當前的線程中。最后就是返回窗口的引用了。
如果你現(xiàn)在就構(gòu)建、運行這個程序,你看不到任何東西。很合理,因為我們還沒有用這個窗口做什么實質(zhì)性的事。
定義一個新函數(shù),初始化 OpenGL,就可以解決這個問題:
// initOpenGL 初始化 OpenGL 并且返回一個初始化了的程序。
func initOpenGL() uint32 {
if err := gl.Init(); err != nil {
panic(err)
}
version := gl.GoStr(gl.GetString(gl.VERSION))
log.Println("OpenGL version", version)
prog := gl.CreateProgram()
gl.LinkProgram(prog)
return prog
}
initOpenGL
就像之前的 initGlfw
函數(shù)一樣,初始化 OpenGL 庫,創(chuàng)建一個程序?!俺绦颉笔且粋€包含了著色器的引用,稍后會用著色器繪圖。待會兒會講這一點,現(xiàn)在只用知道 OpenGL 已經(jīng)初始化完成了,我們有一個程序的引用。我們還打印了 OpenGL 的版本,可以用于之后的調(diào)試。
回到 main
函數(shù)里,調(diào)用這個新函數(shù):
func main() {
runtime.LockOSThread()
window := initGlfw()
defer glfw.Terminate()
program := initOpenGL()
for !window.ShouldClose() {
draw(window, program)
}
}
你應(yīng)該注意到了現(xiàn)在我們有 program
的引用,在我們的窗口循環(huán)中,調(diào)用新的 draw
函數(shù)。最終這個函數(shù)會繪制出所有細胞,讓游戲狀態(tài)變得可視化,但是現(xiàn)在它做的僅僅是清除窗口,所以我們只能看到一個全黑的屏幕:
func draw(window *glfw.Window, program uint32) {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.UseProgram(prog)
glfw.PollEvents()
window.SwapBuffers()
}
我們首先做的是調(diào)用 gl.clear
函數(shù)來清除上一幀在窗口中繪制的東西,給我們一個干凈的面板。然后我們告訴 OpenGL 去使用我們的程序引用,這個引用還沒有做什么事。最終我們告訴 GLFW 用 PollEvents
去檢查是否有鼠標或者鍵盤事件(這一節(jié)里還不會對這些事件進行處理),告訴窗口去交換緩沖區(qū) SwapBuffers
。 交換緩沖區(qū) 很重要,因為 GLFW(像其他圖形庫一樣)使用雙緩沖,也就是說你繪制的所有東西實際上是繪制到一個不可見的畫布上,當你準備好進行展示的時候就把繪制的這些東西放到可見的畫布中 —— 這種情況下,就需要調(diào)用 SwapBuffers
函數(shù)。
好了,到這里我們已經(jīng)講了很多東西,花一點時間看看我們的實驗成果。運行這個程序,你應(yīng)該可以看到你所繪制的第一個東西:
Conway's Game of Life - 第一個窗口
完美!
在窗口里繪制三角形
我們已經(jīng)完成了一些復(fù)雜的步驟,即使看起來不多,但我們?nèi)匀恍枰L制一些東西。我們會以三角形繪制開始,可能這第一眼看上去要比我們最終要繪制的方形更難,但你會知道這樣的想法是錯的。你可能不知道的是三角形或許是繪制的圖形中最簡單的,實際上我們最終會用某種方式把三角形拼成方形。
好吧,那么我們想要繪制一個三角形,怎么做呢?我們通過定義圖形的頂點來繪制圖形,把它們交給 OpenGL 來進行繪制。先在 main.go
的頂部里定義我們的三角形:
var (
triangle = []float32{
0, 0.5, 0, // top
-0.5, -0.5, 0, // left
0.5, -0.5, 0, // right
}
)
這看上去很奇怪,讓我們分開來看。首先我們用了一個 float32
切片,這是一種我們總會在向 OpenGL 傳遞頂點時用到的數(shù)據(jù)類型。這個切片包含 9 個值,每三個值構(gòu)成三角形的一個點。第一行, 0, 0.5, 0
表示的是 X、Y、Z 坐標,是最上方的頂點,第二行是左邊的頂點,第三行是右邊的頂點。每一組的三個點都表示相對于窗口中心點的 X、Y、Z 坐標,大小在 -1
和 1
之間。因此最上面的頂點 X 坐標是 0
,因為它在 X 方向上位于窗口中央,Y 坐標是 0.5
意味著它會相對窗口中央上移 1/4 個單位(因為窗口的范圍是 -1
到 1
),Z 坐標是 0。因為我們只需要在二維空間中繪圖,所以 Z 值永遠是 0
?,F(xiàn)在看一看左右兩邊的頂點,看看你能不能理解為什么它們是這樣定義的 —— 如果不能立刻就弄清楚也沒關(guān)系,我們將會在屏幕上去觀察它,因此我們需要一個完美的圖形來進行觀察。
好了,我們定義了一個三角形,但是現(xiàn)在我們得把它畫出來。要畫出這個三角形,我們需要一個叫做頂點數(shù)組對象或者叫 vao 的東西,這是由一系列的點(也就是我們定義的三角形)創(chuàng)造的,這個東西可以提供給 OpenGL 來進行繪制。創(chuàng)建一個叫做 makeVao
的函數(shù),然后我們可以提供一個點的切片,讓它返回一個指向 OpenGL 頂點數(shù)組對象的指針:
// makeVao 執(zhí)行初始化并從提供的點里面返回一個頂點數(shù)組
func makeVao(points []float32) uint32 {
var vbo uint32
gl.GenBuffers(1, &vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)
var vao uint32
gl.GenVertexArrays(1, &vao)
gl.BindVertexArray(vao)
gl.EnableVertexAttribArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)
return vao
}
首先我們創(chuàng)造了頂點緩沖區(qū)對象 或者說 vbo 綁定到我們的 vao
上,vbo
是通過所占空間(也就是 4 倍 len(points)
大小的空間)和一個指向頂點的指針(gl.Ptr(points)
)來創(chuàng)建的。你也許會好奇為什么它是 4 倍 —— 而不是 6 或者 3 或者 1078 呢?原因在于我們用的是 float32
切片,32 個位的浮點型變量是 4 個字節(jié),因此我們說這個緩沖區(qū)以字節(jié)為單位的大小是點個數(shù)的 4 倍。
現(xiàn)在我們有緩沖區(qū)了,可以創(chuàng)建 vao
并用 gl.BindBuffer
把它綁定到緩沖區(qū)上,最后返回 vao
。這個 vao
將會被用于繪制三角形!
回到 main
函數(shù):
func main() {
...
vao := makeVao(triangle)
for !window.ShouldClose() {
draw(vao, window, program)
}
}
這里我們調(diào)用了 `makeVao` ,從我們之前定義的 `triangle` 頂點中獲得 `vao` 引用,將它作為一個新的參數(shù)傳遞給 `draw` 函數(shù):
func draw(vao uint32, window *glfw.Window, program uint32) {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.UseProgram(program)
gl.BindVertexArray(vao)
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle) / 3))
glfw.PollEvents()
window.SwapBuffers()
}
然后我們把 OpenGL 綁定到 vao
上,這樣當我們告訴 OpenGL 三角形切片的頂點數(shù)(除以 3,是因為每一個點有 X、Y、Z 坐標),讓它去 DrawArrays
,它就知道要畫多少個頂點了。
如果你這時候運行程序,你可能希望在窗口中央看到一個美麗的三角形,但是不幸的是你還看不到。還有一件事情沒做,我們告訴 OpenGL 我們要畫一個三角形,但是我們還要告訴它怎么畫出來。
要讓它畫出來,我們需要叫做片元著色器和頂點著色器的東西,這些已經(jīng)超出本教程的范圍了(老實說,也超出了我對 OpenGL 的了解),但 Harold Serrano 在 Quora 上對對它們是什么給出了完美的介紹。我們只需要理解,對于這個應(yīng)用來說,著色器是它內(nèi)部的小程序(用 OpenGL Shader Language 或 GLSL 編寫的),它操作頂點進行繪制,也可用于確定圖形的顏色。
添加兩個 import
和一個叫做 compileShader
的函數(shù):
import (
"strings"
"fmt"
)
func compileShader(source string, shaderType uint32) (uint32, error) {
shader := gl.CreateShader(shaderType)
csources, free := gl.Strs(source)
gl.ShaderSource(shader, 1, csources, nil)
free()
gl.CompileShader(shader)
var status int32
gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
if status == gl.FALSE {
var logLength int32
gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
log := strings.Repeat("\x00", int(logLength+1))
gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))
return 0, fmt.Errorf("failed to compile %v: %v", source, log)
}
return shader, nil
}
這個函數(shù)的目的是以字符串的形式接受著色器源代碼和它的類型,然后返回一個指向這個編譯好的著色器的指針。如果編譯失敗,我們就會獲得出錯的詳細信息。
現(xiàn)在定義著色器,在 makeProgram
里編譯?;氐轿覀兊?nbsp;const
塊中,我們在這里定義了 width
和 hegiht
。
vertexShaderSource = `
#version 410
in vec3 vp;
void main() {
gl_Position = vec4(vp, 1.0);
}
` + "\x00"
fragmentShaderSource = `
#version 410
out vec4 frag_colour;
void main() {
frag_colour = vec4(1, 1, 1, 1);
}
` + "\x00"
如你所見,這是兩個包含了 GLSL 源代碼字符串的著色器,一個是頂點著色器,另一個是片元著色器。唯一比較特殊的地方是它們都要在末尾加上一個空終止字符,\x00
—— OpenGL 需要它才能編譯著色器。注意 fragmentShaderSource
,這是我們用 RGBA 形式的值通過 vec4
來定義我們圖形的顏色。你可以修改這里的值來改變這個三角形的顏色,現(xiàn)在的值是 RGBA(1, 1, 1, 1)
或者說是白色。
同樣需要注意的是這兩個程序都是運行在 #version 410
版本下,如果你用的是 OpenGL 2.1,那你也可以改成 #version 120
。這里 120
不是打錯的,如果你用的是 OpenGL 2.1,要用 120
而不是 210
!
接下來在 initOpenGL
中我們會編譯著色器,把它們附加到我們的 program
中。
func initOpenGL() uint32 {
if err := gl.Init(); err != nil {
panic(err)
}
version := gl.GoStr(gl.GetString(gl.VERSION))
log.Println("OpenGL version", version)
vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
if err != nil {
panic(err)
}
fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
if err != nil {
panic(err)
}
prog := gl.CreateProgram()
gl.AttachShader(prog, vertexShader)
gl.AttachShader(prog, fragmentShader)
gl.LinkProgram(prog)
return prog
}
這里我們用頂點著色器(vertexShader
)調(diào)用了 compileShader
函數(shù),指定它的類型是 gl.VERTEX_SHADER
,對片元著色器(fragmentShader
)做了同樣的事情,但是指定的類型是 gl.FRAGMENT_SHADER
。編譯完成后,我們把它們附加到程序中,調(diào)用 gl.AttachShader
,傳遞程序(prog
)以及編譯好的著色器作為參數(shù)。
現(xiàn)在我們終于可以看到我們漂亮的三角形了!運行程序,如果一切順利的話你會看到這些:
Conway's Game of Life - Hello, Triangle!
總結(jié)
是不是很驚喜!這些代碼畫出了一個三角形,但我保證我們已經(jīng)完成了大部分的 OpenGL 代碼,在接下來的章節(jié)中我們還會用到這些代碼。我十分推薦你花幾分鐘修改一下代碼,看看你能不能移動三角形,改變?nèi)切蔚拇笮『皖伾penGL 可以令人心生畏懼,有時想要理解發(fā)生了什么很困難,但是要記住,這不是魔法 - 它只不過看上去像魔法。
下一節(jié)里我們講會用兩個銳角三角形拼出一個方形 - 看看你能不能在進入下一節(jié)前試著修改這一節(jié)的代碼。不能也沒有關(guān)系,因為我們在 第二節(jié) 還會編寫代碼, 接著創(chuàng)建一個有許多方形的格子,我們把它當做游戲面板。
最后,在第三節(jié) 里我們會用格子來實現(xiàn) Conway’s Game of Life!
回顧
本教程 main.go
文件的內(nèi)容如下:
package main
import (
"fmt"
"log"
"runtime"
"strings"
"github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
"github.com/go-gl/glfw/v3.2/glfw"
)
const (
width = 500
height = 500
vertexShaderSource = `
#version 410
in vec3 vp;
void main() {
gl_Position = vec4(vp, 1.0);
}
` + "\x00"
fragmentShaderSource = `
#version 410
out vec4 frag_colour;
void main() {
frag_colour = vec4(1, 1, 1, 1.0);
}
` + "\x00"
)
var (
triangle = []float32{
0, 0.5, 0,
-0.5, -0.5, 0,
0.5, -0.5, 0,
}
)
func main() {
runtime.LockOSThread()
window := initGlfw()
defer glfw.Terminate()
program := initOpenGL()
vao := makeVao(triangle)
for !window.ShouldClose() {
draw(vao, window, program)
}
}
func draw(vao uint32, window *glfw.Window, program uint32) {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.UseProgram(program)
gl.BindVertexArray(vao)
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle)/3))
glfw.PollEvents()
window.SwapBuffers()
}
// initGlfw 初始化 glfw 并返回一個窗口供使用。
func initGlfw() *glfw.Window {
if err := glfw.Init(); err != nil {
panic(err)
}
glfw.WindowHint(glfw.Resizable, glfw.False)
glfw.WindowHint(glfw.ContextVersionMajor, 4)
glfw.WindowHint(glfw.ContextVersionMinor, 1)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
if err != nil {
panic(err)
}
window.MakeContextCurrent()
return window
}
// initOpenGL 初始化 OpenGL 并返回一個已經(jīng)編譯好的著色器程序
func initOpenGL() uint32 {
if err := gl.Init(); err != nil {
panic(err)
}
version := gl.GoStr(gl.GetString(gl.VERSION))
log.Println("OpenGL version", version)
vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
if err != nil {
panic(err)
}
fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
if err != nil {
panic(err)
<
精品香蕉一区二区在线|
精品人妻一区二区四区|
男女午夜福利院在线观看|
日韩欧美黄色一级视频|
一区二区日韩欧美精品|
欧美精品在线观看国产|
老司机精品国产在线视频|
久久碰国产一区二区三区|
精品少妇人妻av一区二区蜜桃|
日本免费一区二区三女|
成人国产一区二区三区精品麻豆|
成人欧美精品一区二区三区|
欧美日韩中国性生活视频|
九九热这里有精品20|
偷拍偷窥女厕一区二区视频|
国内真实露脸偷拍视频|
久久精品亚洲精品国产欧美|
天堂网中文字幕在线视频|
老司机精品视频在线免费看
|
麻豆在线观看一区二区|
日本女人亚洲国产性高潮视频|
亚洲精品中文字幕熟女|
日韩欧美一区二区不卡视频|
国产亚洲欧美日韩国亚语|
国产又粗又猛又长又大|
久七久精品视频黄色的|
国产偷拍盗摄一区二区|
人妻亚洲一区二区三区|
日本免费一本一二区三区|
日韩免费av一区二区三区|
国产精品免费不卡视频|
国产在线小视频你懂的|
日韩人妻中文字幕精品|
欧美日韩亚洲精品内裤|
亚洲中文字幕综合网在线|
亚洲国产精品国自产拍社区|
午夜福利视频日本一区|
美日韩一区二区精品系列|
国产欧美日韩在线精品一二区|
暴力三级a特黄在线观看|
日韩av生活片一区二区三区|