Introduction
When I see a new programming language, a question usually tries to enter my head; especially when I see a general purpose programming language. “Does this language support any hardware accelerated graphics library... such as OpenGL or DirectX? So I can make a game or a 3D application using it!” I don’t know about others, but I actually love graphics programming. So when I saw the Google’s Go, the same I thought, looked for an OpenGL package first and found a perfect one on the GitHub website. Then I made a simple project using the OpenGL in Go which shows a cube with basic lighting. Like my previous article on Go which showed a simple window creation with pure Win32 API, I’ve also used the pure Win32 API for the window creation in this project. It actually helps to understand the basic concept, as far as I know. So I thought why not share it with people? Let’s do it!
Downloading Necessary Go Packages
The OpenGL package can be found on GitHub website - https://github.com/go-gl/.
The package provides various version of the OpenGL. There also have several other Go bindings such as osmesa, GLFW 3, OpenCL, etc.
We also need the w32 package to use the Windows APIs. Download it from the following link, if you don’t have it already:
Using the Code
In this article, I’m not going to describe the window creation codes much since I already did it in my previous article on Go titled as ‘Creating A Window In Go Language’. So if you haven’t read it yet, I recommend reading it first before you go further. Here is the link:
First, we import all the necessary packages needed for our program. We will use the 2.1 version of the OpenGL API. We also need to import the unsafe
package in order to obtain any structure’s size. Importing begins with import
keyword:
import (
"runtime"
"syscall"
"unsafe"
"go-libs/w32"
"go-libs/gl/v2.1/gl"
)
Inside the main
function which is the entry point of any Go program, we start creating the main window and then initialize the OpenGL rendering context. Function begins with func
keyword in Go:
func main() {
The following is a bool/boolean
variable, used to determine whether the OpenGL is initialized or not. So at the very beginning of our program execution, we assign false
as its initial value:
GlInitialized = false
The following codes are to create the main window. This window is actually our drawing surface where we draw our 3D scene using the OpenGL rendering context. Go language lets us easily create and assign variable through using :=
hInstance := w32.GetModuleHandle("")
We require calling the StringToUTF16Ptr
function of the syscall
package in order to convert a Go’s string
literal to a UTF-16 string
. Basically here, this string
literal is our window’s class name:
lpszClassName := syscall.StringToUTF16Ptr("GoOpenGL!--Class")
Let us create and fill a WNDCLASSEX
structure with appropriate values:
var wcex w32.WNDCLASSEX
wcex.Size = uint32(unsafe.Sizeof(wcex))
wcex.Style = w32.CS_HREDRAW | w32.CS_VREDRAW
wcex.WndProc = syscall.NewCallback(WndProc)
wcex.ClsExtra = 0
wcex.WndExtra = 0
wcex.Instance = hInstance
wcex.Icon = w32.LoadIcon(hInstance, MakeIntResource(w32.IDI_APPLICATION))
wcex.Cursor = w32.LoadCursor(0, MakeIntResource(w32.IDC_ARROW))
wcex.Background = w32.COLOR_WINDOW + 11
wcex.MenuName = nil
wcex.ClassName = lpszClassName
wcex.IconSm = w32.LoadIcon(hInstance, MakeIntResource(w32.IDI_APPLICATION))
w32.RegisterClassEx(&wcex)
hWnd := w32.CreateWindowEx(
0, lpszClassName, syscall.StringToUTF16Ptr("Go OpenGL!"),
w32.WS_OVERLAPPEDWINDOW | w32.WS_VISIBLE,
w32.CW_USEDEFAULT, w32.CW_USEDEFAULT, 400, 400, 0, 0, hInstance, nil)
In order to setup the pixel format of the drawing surface and create the GL context, we have to obtain our main window’s device context first:
hDC := w32.GetDC(hWnd)
Now, we initialize a pixel format descriptor structure with appropriate values which lets us describe the drawing surface:
var pfd w32.PIXELFORMATDESCRIPTOR
pfd.Size = uint16(unsafe.Sizeof(pfd))
pfd.Version = 1
pfd.DwFlags = w32.PFD_DRAW_TO_WINDOW | w32.PFD_SUPPORT_OPENGL | w32.PFD_DOUBLEBUFFER
pfd.IPixelType = w32.PFD_TYPE_RGBA
We will use 32-bit color for our drawing surface:
pfd.ColorBits = 32
The following ChoosePixelFormat
function will attempt to match an appropriate pixel format supported by the window’s device context to our given pixel format description. It returns the appropriate pixel format index if success otherwise, returns zero on fail:
pf := w32.ChoosePixelFormat(hDC, &pfd)
Now set the window’s device context’s pixel format to the pixel format returned by ChoosePixelFormat
:
w32.SetPixelFormat (hDC, pf, &pfd)
Now, we create the OpenGL rendering context through calling the WglCreateContext
function of the w32
package:
hRC := w32.WglCreateContext(hDC)
w32.WglMakeCurrent(hDC, hRC)
Don’t forget to initialize the gl
package, otherwise none of any OpenGL function will work and could crash the program:
if err := gl.Init(); err != nil {
panic(err)
}
Assign the GlInitialized
with true
value:
GlInitialized = true
We also enable some lighting for a nice and better visual. Without any lighting, our 3D scene will look flat.
OpenGL has Enable
and Disable
function, used to enable/disable any specific feature or functionality. So we use the Enable
function of the gl
package in order to enable OpenGL lighting:
gl.Enable(gl.LIGHTING)
Also set light’s ambient, diffuse and position attributes. You may want to play with these attributes or parameters for the experiment purpose.
In fixed pipeline, the OpenGL supports up to 8 lights and they are indexed as LIGHT0
, LIGHT1
, and LIGHT2
so on. But we only need one light for our 3D scene. So use the first one which is LIGHT0
.
Let’s create our parameter variables and assign appropriate values to them:
ambient := []float32{0.5, 0.5, 0.5, 1}
diffuse := []float32{1, 1, 1, 1}
lightPosition := []float32{-5, 5, 10, 0}
Now tell OpenGL that those parameters are for the LIGHT0
:
gl.Lightfv(gl.LIGHT0, gl.AMBIENT, &ambient[0])
gl.Lightfv(gl.LIGHT0, gl.DIFFUSE, &diffuse[0])
gl.Lightfv(gl.LIGHT0, gl.POSITION, &lightPosition[0])
We also have to enable a light manually in order to use it. So let’s enable the LIGHT0
:
gl.Enable(gl.LIGHT0)
So now, all the initializations are done. It is the time to draw…
Inside our application’s main loop, we draw our scene and then swap the front and back buffers to show the rendered image into the window:
for {
drawScene()
This SwapBuffers
function swaps the front and back buffers if the current pixel format for the window referenced by the specified device context includes a back buffer. So, if we don’t exchange the front and back buffers, nothing will appear in the window:
w32.SwapBuffers(hDC)
}
When the main loop ends, we cleanup the memory:
w32.WglMakeCurrent(w32.HDC(0), w32.HGLRC(0))
w32.ReleaseDC(hWnd, hDC)
w32.WglDeleteContext(hRC)
Also delete the main window we created:
w32.DestroyWindow(hWnd)
}
This drawScene
function clears the window with gray color, setups projection and model-view matrix and then draws a simple cube:
func drawScene() {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.ClearColor(0.5, 0.5, 0.5, 0.0)
gl.ClearDepth(1)
gl.MatrixMode(gl.PROJECTION)
gl.LoadIdentity()
gl.Frustum(-1, 1, -1, 1, 1.0, 10.0)
gl.MatrixMode(gl.MODELVIEW)
gl.LoadIdentity()
Let’s slightly rotate the view for a 3D look and feel:
gl.Translatef(0, 0, -3.0)
gl.Rotatef(40.0, 1, 0, 0)
gl.Rotatef(40.0, 0, 1, 0)
Now we draw a simple cube:
drawCube()
}
We also have to handle the WM_SIZE
message to resize the OpenGL viewport according to our main window size:
func WndProc(hWnd w32.HWND, msg uint32, wParam, lParam uintptr) (uintptr) {
switch msg {
case w32.WM_SIZE:
Make sure we have initialized the OpenGL before we use it (since the WM_SIZE
message will immediately appear here after CreateWindowEx
function creates the window):
if GlInitialized == true {
rc := w32.GetClientRect(hWnd)
gl.Viewport(0, 0, rc.Right, rc.Bottom)
}
break
Building the Program
Finally, we compile our program using the following commands. These commands can/should be putted in a batch (.bat) file for shortcut build:
go build -ldflags "-H windowsgui" main.go
Now, run the ‘main.exe’ file to execute the program and see your ‘Go OpenGL!’
Request to Readers
If you have any ideas about a new project on Go language that would help people to learn the language more easily and effectively, you can inform me through commenting. Also feel free to tell me if you have any suggestion on improving this article.
Conclusion
Go is a very nice general purpose programming language. There is a lot to learn about it and we should use it wisely to get proper benefits from it. And at the end, I hope my article has helped beginner programmers.