一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

OpenGL 與 Go 教程(一)Hello, OpenGL

 KyunraWang 2018-05-08


2017-10-05 21:27    

這篇教程的所有源代碼都可以在 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 上獲得,當你有疑惑的時候可以隨時查看源代碼,或者你要按照自己的方式學習也可以參考這個代碼。

在我們開始之前,我們要先弄明白康威生命游戲Conway's Game of Life 到底是什么。這里是 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ā)生:

  1. 當任何一個存活的細胞的附近少于 2 個存活的細胞時,該細胞將會消亡,就像人口過少所導(dǎo)致的結(jié)果一樣
  2. 當任何一個存活的細胞的附近有 2 至 3 個存活的細胞時,該細胞在下一代中仍然存活。
  3. 當任何一個存活的細胞的附近多于 3 個存活的細胞時,該細胞將會消亡,就像人口過多所導(dǎo)致的結(jié)果一樣
  4. 任何一個消亡的細胞附近剛好有 3 個存活的細胞,該細胞會變?yōu)榇婊畹臓顟B(tài),就像重生一樣。

不需要其他工具,這里有一個我們將會制作的演示程序:

Conway's Game of Life - 示例游戲

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,我們需要做這些事:

  1. # 對于 OpenGL 4.1
  2. $ go get github.com/go-gl/gl/v4.1-core/gl
  3. # 或者 2.1
  4. $ go get github.com/go-gl/gl/v2.1/gl

然后是安裝 GLFW:

  1. $ go get github.com/go-gl/glfw/v3.2/glfw

安裝好這兩個包之后,我們就可以開始了!先創(chuàng)建 main.go 文件,導(dǎo)入相應(yīng)的包(我們待會兒會用到的其它東西)。

  1. package main
  2. import (
  3. "log"
  4. "runtime"
  5. "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
  6. "github.com/go-gl/glfw/v3.2/glfw"
  7. )

接下來定義一個叫做 main 的函數(shù),這是用來初始化 OpenGL 以及 GLFW,并顯示窗口的:

  1. const (
  2. width = 500
  3. height = 500
  4. )
  5. func main() {
  6. runtime.LockOSThread()
  7. window := initGlfw()
  8. defer glfw.Terminate()
  9. for !window.ShouldClose() {
  10. // TODO
  11. }
  12. }
  13. // initGlfw 初始化 glfw 并且返回一個可用的窗口。
  14. func initGlfw() *glfw.Window {
  15. if err := glfw.Init(); err != nil {
  16. panic(err)
  17. }
  18. glfw.WindowHint(glfw.Resizable, glfw.False)
  19. glfw.WindowHint(glfw.ContextVersionMajor, 4) // OR 2
  20. glfw.WindowHint(glfw.ContextVersionMinor, 1)
  21. glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
  22. glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
  23. window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
  24. if err != nil {
  25. panic(err)
  26. }
  27. window.MakeContextCurrent()
  28. return window
  29. }

好了,讓我們花一分鐘來運行一下這個程序,看看會發(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,就可以解決這個問題:

  1. // initOpenGL 初始化 OpenGL 并且返回一個初始化了的程序。
  2. func initOpenGL() uint32 {
  3. if err := gl.Init(); err != nil {
  4. panic(err)
  5. }
  6. version := gl.GoStr(gl.GetString(gl.VERSION))
  7. log.Println("OpenGL version", version)
  8. prog := gl.CreateProgram()
  9. gl.LinkProgram(prog)
  10. return prog
  11. }

initOpenGL 就像之前的 initGlfw 函數(shù)一樣,初始化 OpenGL 庫,創(chuàng)建一個程序program?!俺绦颉笔且粋€包含了著色器shader的引用,稍后會用著色器shader繪圖。待會兒會講這一點,現(xiàn)在只用知道 OpenGL 已經(jīng)初始化完成了,我們有一個程序的引用。我們還打印了 OpenGL 的版本,可以用于之后的調(diào)試。

回到 main 函數(shù)里,調(diào)用這個新函數(shù):

  1. func main() {
  2. runtime.LockOSThread()
  3. window := initGlfw()
  4. defer glfw.Terminate()
  5. program := initOpenGL()
  6. for !window.ShouldClose() {
  7. draw(window, program)
  8. }
  9. }

你應(yīng)該注意到了現(xiàn)在我們有 program 的引用,在我們的窗口循環(huán)中,調(diào)用新的 draw 函數(shù)。最終這個函數(shù)會繪制出所有細胞,讓游戲狀態(tài)變得可視化,但是現(xiàn)在它做的僅僅是清除窗口,所以我們只能看到一個全黑的屏幕:

  1. func draw(window *glfw.Window, program uint32) {
  2. gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  3. gl.UseProgram(prog)
  4. glfw.PollEvents()
  5. window.SwapBuffers()
  6. }

我們首先做的是調(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 - 第一個窗口

Conway's Game of Life - 第一個窗口

完美!

在窗口里繪制三角形

我們已經(jīng)完成了一些復(fù)雜的步驟,即使看起來不多,但我們?nèi)匀恍枰L制一些東西。我們會以三角形繪制開始,可能這第一眼看上去要比我們最終要繪制的方形更難,但你會知道這樣的想法是錯的。你可能不知道的是三角形或許是繪制的圖形中最簡單的,實際上我們最終會用某種方式把三角形拼成方形。

好吧,那么我們想要繪制一個三角形,怎么做呢?我們通過定義圖形的頂點來繪制圖形,把它們交給 OpenGL 來進行繪制。先在 main.go 的頂部里定義我們的三角形:

  1. var (
  2. triangle = []float32{
  3. 0, 0.5, 0, // top
  4. -0.5, -0.5, 0, // left
  5. 0.5, -0.5, 0, // right
  6. }
  7. )

這看上去很奇怪,讓我們分開來看。首先我們用了一個 float32 切片slice,這是一種我們總會在向 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ù)組對象Vertex Array Object或者叫 vao 的東西,這是由一系列的點(也就是我們定義的三角形)創(chuàng)造的,這個東西可以提供給 OpenGL 來進行繪制。創(chuàng)建一個叫做 makeVao 的函數(shù),然后我們可以提供一個點的切片,讓它返回一個指向 OpenGL 頂點數(shù)組對象的指針:

  1. // makeVao 執(zhí)行初始化并從提供的點里面返回一個頂點數(shù)組
  2. func makeVao(points []float32) uint32 {
  3. var vbo uint32
  4. gl.GenBuffers(1, &vbo)
  5. gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
  6. gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)
  7. var vao uint32
  8. gl.GenVertexArrays(1, &vao)
  9. gl.BindVertexArray(vao)
  10. gl.EnableVertexAttribArray(0)
  11. gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
  12. gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)
  13. return vao
  14. }

首先我們創(chuàng)造了頂點緩沖區(qū)對象Vertex Buffer Object 或者說 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ù):

  1. func main() {
  2. ...
  3. vao := makeVao(triangle)
  4. for !window.ShouldClose() {
  5. draw(vao, window, program)
  6. }
  7. }
  8. 這里我們調(diào)用了 `makeVao` ,從我們之前定義的 `triangle` 頂點中獲得 `vao` 引用,將它作為一個新的參數(shù)傳遞給 `draw` 函數(shù):
  9. func draw(vao uint32, window *glfw.Window, program uint32) {
  10. gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  11. gl.UseProgram(program)
  12. gl.BindVertexArray(vao)
  13. gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle) / 3))
  14. glfw.PollEvents()
  15. window.SwapBuffers()
  16. }

然后我們把 OpenGL 綁定到 vao 上,這樣當我們告訴 OpenGL 三角形切片的頂點數(shù)(除以 3,是因為每一個點有 X、Y、Z 坐標),讓它去 DrawArrays ,它就知道要畫多少個頂點了。

如果你這時候運行程序,你可能希望在窗口中央看到一個美麗的三角形,但是不幸的是你還看不到。還有一件事情沒做,我們告訴 OpenGL 我們要畫一個三角形,但是我們還要告訴它怎么畫出來。

要讓它畫出來,我們需要叫做片元著色器fragment shader頂點著色器vertex shader的東西,這些已經(jīng)超出本教程的范圍了(老實說,也超出了我對 OpenGL 的了解),但 Harold Serrano 在 Quora 上對對它們是什么給出了完美的介紹。我們只需要理解,對于這個應(yīng)用來說,著色器是它內(nèi)部的小程序(用 OpenGL Shader Language 或 GLSL 編寫的),它操作頂點進行繪制,也可用于確定圖形的顏色。

添加兩個 import 和一個叫做 compileShader 的函數(shù):

  1. import (
  2. "strings"
  3. "fmt"
  4. )
  5. func compileShader(source string, shaderType uint32) (uint32, error) {
  6. shader := gl.CreateShader(shaderType)
  7. csources, free := gl.Strs(source)
  8. gl.ShaderSource(shader, 1, csources, nil)
  9. free()
  10. gl.CompileShader(shader)
  11. var status int32
  12. gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
  13. if status == gl.FALSE {
  14. var logLength int32
  15. gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
  16. log := strings.Repeat("\x00", int(logLength+1))
  17. gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))
  18. return 0, fmt.Errorf("failed to compile %v: %v", source, log)
  19. }
  20. return shader, nil
  21. }

這個函數(shù)的目的是以字符串的形式接受著色器源代碼和它的類型,然后返回一個指向這個編譯好的著色器的指針。如果編譯失敗,我們就會獲得出錯的詳細信息。

現(xiàn)在定義著色器,在 makeProgram 里編譯?;氐轿覀兊?nbsp;const 塊中,我們在這里定義了 width 和 hegiht

  1. vertexShaderSource = `
  2. #version 410
  3. in vec3 vp;
  4. void main() {
  5. gl_Position = vec4(vp, 1.0);
  6. }
  7. ` + "\x00"
  8. fragmentShaderSource = `
  9. #version 410
  10. out vec4 frag_colour;
  11. void main() {
  12. frag_colour = vec4(1, 1, 1, 1);
  13. }
  14. ` + "\x00"

如你所見,這是兩個包含了 GLSL 源代碼字符串的著色器,一個是頂點著色器vertex shader,另一個是片元著色器fragment shader。唯一比較特殊的地方是它們都要在末尾加上一個空終止字符,\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 中。

  1. func initOpenGL() uint32 {
  2. if err := gl.Init(); err != nil {
  3. panic(err)
  4. }
  5. version := gl.GoStr(gl.GetString(gl.VERSION))
  6. log.Println("OpenGL version", version)
  7. vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
  8. if err != nil {
  9. panic(err)
  10. }
  11. fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
  12. if err != nil {
  13. panic(err)
  14. }
  15. prog := gl.CreateProgram()
  16. gl.AttachShader(prog, vertexShader)
  17. gl.AttachShader(prog, fragmentShader)
  18. gl.LinkProgram(prog)
  19. return prog
  20. }

這里我們用頂點著色器(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!

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)容如下:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "runtime"
  6. "strings"
  7. "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
  8. "github.com/go-gl/glfw/v3.2/glfw"
  9. )
  10. const (
  11. width = 500
  12. height = 500
  13. vertexShaderSource = `
  14. #version 410
  15. in vec3 vp;
  16. void main() {
  17. gl_Position = vec4(vp, 1.0);
  18. }
  19. ` + "\x00"
  20. fragmentShaderSource = `
  21. #version 410
  22. out vec4 frag_colour;
  23. void main() {
  24. frag_colour = vec4(1, 1, 1, 1.0);
  25. }
  26. ` + "\x00"
  27. )
  28. var (
  29. triangle = []float32{
  30. 0, 0.5, 0,
  31. -0.5, -0.5, 0,
  32. 0.5, -0.5, 0,
  33. }
  34. )
  35. func main() {
  36. runtime.LockOSThread()
  37. window := initGlfw()
  38. defer glfw.Terminate()
  39. program := initOpenGL()
  40. vao := makeVao(triangle)
  41. for !window.ShouldClose() {
  42. draw(vao, window, program)
  43. }
  44. }
  45. func draw(vao uint32, window *glfw.Window, program uint32) {
  46. gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  47. gl.UseProgram(program)
  48. gl.BindVertexArray(vao)
  49. gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle)/3))
  50. glfw.PollEvents()
  51. window.SwapBuffers()
  52. }
  53. // initGlfw 初始化 glfw 并返回一個窗口供使用。
  54. func initGlfw() *glfw.Window {
  55. if err := glfw.Init(); err != nil {
  56. panic(err)
  57. }
  58. glfw.WindowHint(glfw.Resizable, glfw.False)
  59. glfw.WindowHint(glfw.ContextVersionMajor, 4)
  60. glfw.WindowHint(glfw.ContextVersionMinor, 1)
  61. glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
  62. glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
  63. window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
  64. if err != nil {
  65. panic(err)
  66. }
  67. window.MakeContextCurrent()
  68. return window
  69. }
  70. // initOpenGL 初始化 OpenGL 并返回一個已經(jīng)編譯好的著色器程序
  71. func initOpenGL() uint32 {
  72. if err := gl.Init(); err != nil {
  73. panic(err)
  74. }
  75. version := gl.GoStr(gl.GetString(gl.VERSION))
  76. log.Println("OpenGL version", version)
  77. vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
  78. if err != nil {
  79. panic(err)
  80. }
  81. fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
  82. if err != nil {
  83. panic(err)
  84. < 精品香蕉一区二区在线| 精品人妻一区二区四区| 男女午夜福利院在线观看| 日韩欧美黄色一级视频| 一区二区日韩欧美精品| 欧美精品在线观看国产| 老司机精品国产在线视频| 久久碰国产一区二区三区| 精品少妇人妻av一区二区蜜桃| 日本免费一区二区三女| 成人国产一区二区三区精品麻豆| 成人欧美精品一区二区三区| 欧美日韩中国性生活视频| 九九热这里有精品20| 偷拍偷窥女厕一区二区视频| 国内真实露脸偷拍视频| 久久精品亚洲精品国产欧美| 天堂网中文字幕在线视频| 老司机精品视频在线免费看 | 麻豆在线观看一区二区| 日本女人亚洲国产性高潮视频| 亚洲精品中文字幕熟女| 日韩欧美一区二区不卡视频| 国产亚洲欧美日韩国亚语| 国产又粗又猛又长又大| 久七久精品视频黄色的| 国产偷拍盗摄一区二区| 人妻亚洲一区二区三区| 日本免费一本一二区三区| 日韩免费av一区二区三区| 国产精品免费不卡视频| 国产在线小视频你懂的| 日韩人妻中文字幕精品| 欧美日韩亚洲精品内裤| 亚洲中文字幕综合网在线| 亚洲国产精品国自产拍社区| 午夜福利视频日本一区| 美日韩一区二区精品系列| 国产欧美日韩在线精品一二区| 暴力三级a特黄在线观看| 日韩av生活片一区二区三区|