diff --git a/engine/engine.go b/engine/engine.go index 2192238..86409e2 100755 --- a/engine/engine.go +++ b/engine/engine.go @@ -1,8 +1,10 @@ package engine import ( + "github.com/bloeys/nmage/input" "github.com/bloeys/nmage/timing" "github.com/go-gl/gl/v4.1-core/gl" + "github.com/inkyblackness/imgui-go/v4" "github.com/veandco/go-sdl2/sdl" ) @@ -11,6 +13,72 @@ type Window struct { GlCtx sdl.GLContext } +func (w *Window) handleInputs() { + + input.EventLoopStart() + imIO := imgui.CurrentIO() + + for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { + + switch e := event.(type) { + + case *sdl.MouseWheelEvent: + + input.HandleMouseWheelEvent(e) + + xDelta, yDelta := input.GetMouseWheelMotion() + imIO.AddMouseWheelDelta(float32(xDelta), float32(yDelta)) + + case *sdl.KeyboardEvent: + input.HandleKeyboardEvent(e) + + if e.Type == sdl.KEYDOWN { + imIO.KeyPress(int(e.Keysym.Scancode)) + } else if e.Type == sdl.KEYUP { + imIO.KeyRelease(int(e.Keysym.Scancode)) + } + + case *sdl.TextInputEvent: + imIO.AddInputCharacters(string(e.Text[:])) + + case *sdl.MouseButtonEvent: + input.HandleMouseBtnEvent(e) + + case *sdl.MouseMotionEvent: + input.HandleMouseMotionEvent(e) + + case *sdl.WindowEvent: + if e.Event == sdl.WINDOWEVENT_SIZE_CHANGED { + w.handleWindowResize() + } + + case *sdl.QuitEvent: + input.HandleQuitEvent(e) + } + } + + // If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame. + x, y, _ := sdl.GetMouseState() + imIO.SetMousePosition(imgui.Vec2{X: float32(x), Y: float32(y)}) + + imIO.SetMouseButtonDown(0, input.MouseDown(sdl.BUTTON_LEFT)) + imIO.SetMouseButtonDown(1, input.MouseDown(sdl.BUTTON_RIGHT)) + imIO.SetMouseButtonDown(2, input.MouseDown(sdl.BUTTON_MIDDLE)) + + imIO.KeyShift(sdl.SCANCODE_LSHIFT, sdl.SCANCODE_RSHIFT) + imIO.KeyCtrl(sdl.SCANCODE_LCTRL, sdl.SCANCODE_RCTRL) + imIO.KeyAlt(sdl.SCANCODE_LALT, sdl.SCANCODE_RALT) +} + +func (w *Window) handleWindowResize() { + + fbWidth, fbHeight := w.SDLWin.GLGetDrawableSize() + if fbWidth <= 0 || fbHeight <= 0 { + return + } + gl.Viewport(0, 0, fbWidth, fbHeight) +} + func (w *Window) Destroy() error { return w.SDLWin.Destroy() } diff --git a/engine/game.go b/engine/game.go new file mode 100755 index 0000000..1c246a6 --- /dev/null +++ b/engine/game.go @@ -0,0 +1,53 @@ +package engine + +import ( + "github.com/bloeys/nmage/timing" + nmageimgui "github.com/bloeys/nmage/ui/imgui" + "github.com/go-gl/gl/v4.1-core/gl" +) + +type Game interface { + Init() + + FrameStart() + Update() + Render() + FrameEnd() + ShouldRun() bool + + GetWindow() *Window + GetImGUI() nmageimgui.ImguiInfo + + Deinit() +} + +func Run(g Game) { + + g.Init() + + w := g.GetWindow() + ui := g.GetImGUI() + for g.ShouldRun() { + + width, height := w.SDLWin.GetSize() + fbWidth, fbHeight := w.SDLWin.GLGetDrawableSize() + + timing.FrameStarted() + w.handleInputs() + ui.FrameStart(float32(width), float32(height)) + + g.FrameStart() + + g.Update() + + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + g.Render() + ui.Render(float32(width), float32(height), fbWidth, fbHeight) + w.SDLWin.GLSwap() + + g.FrameEnd() + timing.FrameEnded() + } + + g.Deinit() +} diff --git a/input/input.go b/input/input.go index 33b74be..9bec372 100755 --- a/input/input.go +++ b/input/input.go @@ -31,10 +31,11 @@ type mouseWheelState struct { } var ( - keyMap = make(map[sdl.Keycode]*keyState) - mouseBtnMap = make(map[int]*mouseBtnState) - mouseMotion = mouseMotionState{} - mouseWheel = mouseWheelState{} + keyMap = make(map[sdl.Keycode]*keyState) + mouseBtnMap = make(map[int]*mouseBtnState) + mouseMotion = mouseMotionState{} + mouseWheel = mouseWheelState{} + quitRequested bool ) func EventLoopStart() { @@ -55,6 +56,16 @@ func EventLoopStart() { mouseWheel.XDelta = 0 mouseWheel.YDelta = 0 + + quitRequested = false +} + +func HandleQuitEvent(e *sdl.QuitEvent) { + quitRequested = true +} + +func IsQuitClicked() bool { + return quitRequested } func HandleKeyboardEvent(e *sdl.KeyboardEvent) { diff --git a/main.go b/main.go index 6a69955..ea62f47 100755 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "runtime" "github.com/bloeys/assimp-go/asig" @@ -13,75 +12,50 @@ import ( "github.com/bloeys/nmage/materials" "github.com/bloeys/nmage/meshes" "github.com/bloeys/nmage/timing" + nmageimgui "github.com/bloeys/nmage/ui/imgui" "github.com/go-gl/gl/v4.1-core/gl" "github.com/inkyblackness/imgui-go/v4" "github.com/veandco/go-sdl2/sdl" ) //TODO: Tasks: -//Engine loop //Proper rendering setup -//Flesh out the material system -//Object -//Abstract UI +//Entities and components +//Camera class //Audio +//Flesh out the material system //Low Priority: -// Proper Asset loading - -type ImguiInfo struct { - imCtx *imgui.Context - - vaoID uint32 - vboID uint32 - indexBufID uint32 - texID uint32 -} +// Abstract UI +// Proper Asset loading var ( - winWidth int32 = 1280 - winHeight int32 = 720 - isRunning bool = true window *engine.Window simpleMat *materials.Material - imguiMat *materials.Material cubeMesh *meshes.Mesh modelMat = gglm.NewTrMatId() projMat = &gglm.Mat4{} - imguiInfo *ImguiInfo - camPos = gglm.NewVec3(0, 0, -10) camForward = gglm.NewVec3(0, 0, 1) ) -func main() { +type OurGame struct { + Win *engine.Window + ImGUIInfo nmageimgui.ImguiInfo + Quitting bool +} - runtime.LockOSThread() - - //Init engine - err := engine.Init() - if err != nil { - logging.ErrLog.Fatalln("Failed to init nMage. Err:", err) - } - - //Create window - window, err = engine.CreateOpenGLWindowCentered("nMage", winWidth, winHeight, engine.WindowFlags_RESIZABLE) - if err != nil { - logging.ErrLog.Fatalln("Failed to create window. Err: ", err) - } - defer window.Destroy() - - engine.SetVSync(false) +func (g *OurGame) Init() { //Create materials simpleMat = materials.NewMaterial("Simple Mat", "./res/shaders/simple") - imguiMat = materials.NewMaterial("ImGUI Mat", "./res/shaders/imgui") //Load meshes + var err error cubeMesh, err = meshes.NewMesh("Cube", "./res/models/tex-cube.fbx", asig.PostProcess(0)) if err != nil { logging.ErrLog.Fatalln("Failed to load cube mesh. Err: ", err) @@ -99,8 +73,6 @@ func main() { simpleMat.AddTextureID(v) } - initImGUI() - //Enable vertex attributes simpleMat.SetAttribute(cubeMesh.Buf) @@ -116,182 +88,28 @@ func main() { updateViewMat() //Perspective/Depth - projMat := gglm.Perspective(45*gglm.Deg2Rad, float32(winWidth)/float32(winHeight), 0.1, 500) + projMat := gglm.Perspective(45*gglm.Deg2Rad, float32(1280)/float32(720), 0.1, 500) simpleMat.SetUnifMat4("projMat", projMat) //Lights simpleMat.SetUnifVec3("lightPos1", &lightPos1) simpleMat.SetUnifVec3("lightColor1", &lightColor1) - - //Game loop - for isRunning { - - timing.FrameStarted() - - handleInputs() - runGameLogic() - - draw() - - timing.FrameEnded() - - window.SDLWin.SetTitle(fmt.Sprintf("FPS: %.0f; Elapsed: %v", 1/timing.DT(), timing.ElapsedTime())) - } } -func updateViewMat() { - targetPos := camPos.Clone().Add(camForward) - viewMat := gglm.LookAt(camPos, targetPos, gglm.NewVec3(0, 1, 0)) - simpleMat.SetUnifMat4("viewMat", &viewMat.Mat4) +func (g *OurGame) FrameStart() { } -func initImGUI() { +func (g *OurGame) Update() { - imguiInfo = &ImguiInfo{ - imCtx: imgui.CreateContext(nil), + if input.IsQuitClicked() { + g.Quitting = true } - imIO := imgui.CurrentIO() - imIO.SetBackendFlags(imIO.GetBackendFlags() | imgui.BackendFlagsRendererHasVtxOffset) - - gl.GenVertexArrays(1, &imguiInfo.vaoID) - gl.GenBuffers(1, &imguiInfo.vboID) - gl.GenBuffers(1, &imguiInfo.indexBufID) - gl.GenTextures(1, &imguiInfo.texID) - - // Upload font to gpu - gl.BindTexture(gl.TEXTURE_2D, imguiInfo.texID) - gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) - gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) - gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0) - - image := imIO.Fonts().TextureDataAlpha8() - gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(image.Width), int32(image.Height), 0, gl.RED, gl.UNSIGNED_BYTE, image.Pixels) - - // Store our identifier - imIO.Fonts().SetTextureID(imgui.TextureID(imguiInfo.texID)) - - //Shader attributes - imguiMat.Bind() - imguiMat.EnableAttribute("Position") - imguiMat.EnableAttribute("UV") - imguiMat.EnableAttribute("Color") - imguiMat.UnBind() - - //Init imgui input mapping - keys := map[int]int{ - imgui.KeyTab: sdl.SCANCODE_TAB, - imgui.KeyLeftArrow: sdl.SCANCODE_LEFT, - imgui.KeyRightArrow: sdl.SCANCODE_RIGHT, - imgui.KeyUpArrow: sdl.SCANCODE_UP, - imgui.KeyDownArrow: sdl.SCANCODE_DOWN, - imgui.KeyPageUp: sdl.SCANCODE_PAGEUP, - imgui.KeyPageDown: sdl.SCANCODE_PAGEDOWN, - imgui.KeyHome: sdl.SCANCODE_HOME, - imgui.KeyEnd: sdl.SCANCODE_END, - imgui.KeyInsert: sdl.SCANCODE_INSERT, - imgui.KeyDelete: sdl.SCANCODE_DELETE, - imgui.KeyBackspace: sdl.SCANCODE_BACKSPACE, - imgui.KeySpace: sdl.SCANCODE_BACKSPACE, - imgui.KeyEnter: sdl.SCANCODE_RETURN, - imgui.KeyEscape: sdl.SCANCODE_ESCAPE, - imgui.KeyA: sdl.SCANCODE_A, - imgui.KeyC: sdl.SCANCODE_C, - imgui.KeyV: sdl.SCANCODE_V, - imgui.KeyX: sdl.SCANCODE_X, - imgui.KeyY: sdl.SCANCODE_Y, - imgui.KeyZ: sdl.SCANCODE_Z, - } - - // Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array. - for imguiKey, nativeKey := range keys { - imIO.KeyMap(imguiKey, nativeKey) - } -} - -func handleInputs() { - - input.EventLoopStart() - imIO := imgui.CurrentIO() - - for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { - - switch e := event.(type) { - - case *sdl.MouseWheelEvent: - - input.HandleMouseWheelEvent(e) - - xDelta, yDelta := input.GetMouseWheelMotion() - imIO.AddMouseWheelDelta(float32(xDelta), float32(yDelta)) - - case *sdl.KeyboardEvent: - input.HandleKeyboardEvent(e) - - if e.Type == sdl.KEYDOWN { - imIO.KeyPress(int(e.Keysym.Scancode)) - } else if e.Type == sdl.KEYUP { - imIO.KeyRelease(int(e.Keysym.Scancode)) - } - - case *sdl.TextInputEvent: - imIO.AddInputCharacters(string(e.Text[:])) - - case *sdl.MouseButtonEvent: - input.HandleMouseBtnEvent(e) - - case *sdl.MouseMotionEvent: - input.HandleMouseMotionEvent(e) - - case *sdl.QuitEvent: - isRunning = false - } - } - - currWinWidth, currWinHeight := window.SDLWin.GetSize() - if winWidth != currWinWidth || winHeight != currWinHeight { - handleWindowResize(currWinWidth, currWinHeight) - } - - // If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame. - x, y, _ := sdl.GetMouseState() - imIO.SetMousePosition(imgui.Vec2{X: float32(x), Y: float32(y)}) - - imIO.SetMouseButtonDown(0, input.MouseDown(sdl.BUTTON_LEFT)) - imIO.SetMouseButtonDown(1, input.MouseDown(sdl.BUTTON_RIGHT)) - imIO.SetMouseButtonDown(2, input.MouseDown(sdl.BUTTON_MIDDLE)) - - imIO.KeyShift(sdl.SCANCODE_LSHIFT, sdl.SCANCODE_RSHIFT) - imIO.KeyCtrl(sdl.SCANCODE_LCTRL, sdl.SCANCODE_RCTRL) - imIO.KeyAlt(sdl.SCANCODE_LALT, sdl.SCANCODE_RALT) -} - -func handleWindowResize(newWinWidth, newWinHeight int32) { - - winWidth = newWinWidth - winHeight = newWinHeight - - fbWidth, fbHeight := window.SDLWin.GLGetDrawableSize() - if fbWidth <= 0 || fbHeight <= 0 { - return - } - gl.Viewport(0, 0, fbWidth, fbHeight) - + winWidth, winHeight := g.Win.SDLWin.GetSize() projMat = gglm.Perspective(45*gglm.Deg2Rad, float32(winWidth)/float32(winHeight), 0.1, 20) simpleMat.SetUnifMat4("projMat", projMat) -} - -var time uint64 = 0 -var name string = "" - -var ambientColor gglm.Vec3 = *gglm.NewVec3(1, 1, 1) -var ambientColorStrength float32 = 0.1 - -var lightPos1 gglm.Vec3 = *gglm.NewVec3(2, 2, 0) -var lightColor1 gglm.Vec3 = *gglm.NewVec3(1, 1, 1) - -func runGameLogic() { + //Camera movement var camSpeed float32 = 15 if input.KeyDown(sdl.K_w) { camPos.Data[1] += camSpeed * timing.DT() @@ -318,47 +136,14 @@ func runGameLogic() { updateViewMat() } + //Rotating cubes modelMat.Rotate(10*timing.DT()*gglm.Deg2Rad, gglm.NewVec3(1, 1, 1).Normalize()) simpleMat.SetUnifMat4("modelMat", &modelMat.Mat4) - //ImGUI - imIO := imgui.CurrentIO() - imIO.SetDisplaySize(imgui.Vec2{X: float32(winWidth), Y: float32(winHeight)}) - - // Setup time step (we don't use SDL_GetTicks() because it is using millisecond resolution) - frequency := sdl.GetPerformanceFrequency() - currentTime := sdl.GetPerformanceCounter() - if time > 0 { - imIO.SetDeltaTime(float32(currentTime-time) / float32(frequency)) - } else { - imIO.SetDeltaTime(1.0 / 60.0) - } - time = currentTime - - imgui.NewFrame() - - if imgui.SliderFloat3("Ambient Color", &ambientColor.Data, 0, 1) { - simpleMat.SetUnifVec3("ambientLightColor", &ambientColor) - } - - if imgui.SliderFloat("Ambient Color Strength", &ambientColorStrength, 0, 1) { - simpleMat.SetUnifFloat32("ambientStrength", ambientColorStrength) - } - - if imgui.SliderFloat3("Light Pos 1", &lightPos1.Data, -10, 10) { - simpleMat.SetUnifVec3("lightPos1", &lightPos1) - } - - if imgui.SliderFloat3("Light Color 1", &lightColor1.Data, 0, 1) { - simpleMat.SetUnifVec3("lightColor1", &lightColor1) - } - - imgui.Render() + imgui.DragFloat3("Cam Pos", &camPos.Data) } -func draw() { - - gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) +func (g *OurGame) Render() { simpleMat.Bind() cubeMesh.Buf.Bind() @@ -373,95 +158,67 @@ func draw() { simpleMat.SetUnifMat4("modelMat", &tempModelMat.Translate(gglm.NewVec3(float32(rowSize), -1, 0)).Mat4) } simpleMat.SetUnifMat4("modelMat", &modelMat.Mat4) - - drawUI() - window.SDLWin.GLSwap() } -func drawUI() { +func (g *OurGame) FrameEnd() { - // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) - fbWidth, fbHeight := window.SDLWin.GLGetDrawableSize() - if fbWidth <= 0 || fbHeight <= 0 { - return - } - - drawData := imgui.RenderedDrawData() - drawData.ScaleClipRects(imgui.Vec2{ - X: float32(fbWidth) / float32(winWidth), - Y: float32(fbHeight) / float32(winHeight), - }) - - // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill - gl.Enable(gl.BLEND) - gl.BlendEquation(gl.FUNC_ADD) - gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) - gl.Disable(gl.CULL_FACE) - gl.Disable(gl.DEPTH_TEST) - gl.Enable(gl.SCISSOR_TEST) - gl.PolygonMode(gl.FRONT_AND_BACK, gl.FILL) - - // Setup viewport, orthographic projection matrix - // Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). - // DisplayMin is typically (0,0) for single viewport apps. - - imguiMat.Bind() - - gl.Uniform1i(gl.GetUniformLocation(imguiMat.ShaderProg.ID, gl.Str("Texture\x00")), 0) - - //PERF: only update the ortho matrix on window resize - orthoMat := gglm.Ortho(0, float32(winWidth), 0, float32(winHeight), 0, 20) - imguiMat.SetUnifMat4("ProjMtx", &orthoMat.Mat4) - gl.BindSampler(0, 0) // Rely on combined texture/sampler state. - - // Recreate the VAO every time - // (This is to easily allow multiple GL contexts. VAO are not shared among GL contexts, and - // we don't track creation/deletion of windows so we don't have an obvious key to use to cache them.) - gl.BindVertexArray(imguiInfo.vaoID) - gl.BindBuffer(gl.ARRAY_BUFFER, imguiInfo.vboID) - - vertexSize, vertexOffsetPos, vertexOffsetUv, vertexOffsetCol := imgui.VertexBufferLayout() - imguiMat.EnableAttribute("Position") - imguiMat.EnableAttribute("UV") - imguiMat.EnableAttribute("Color") - gl.VertexAttribPointerWithOffset(uint32(imguiMat.GetAttribLoc("Position")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetPos)) - gl.VertexAttribPointerWithOffset(uint32(imguiMat.GetAttribLoc("UV")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetUv)) - gl.VertexAttribPointerWithOffset(uint32(imguiMat.GetAttribLoc("Color")), 4, gl.UNSIGNED_BYTE, true, int32(vertexSize), uintptr(vertexOffsetCol)) - - indexSize := imgui.IndexBufferLayout() - drawType := gl.UNSIGNED_SHORT - if indexSize == 4 { - drawType = gl.UNSIGNED_INT - } - - // Draw - for _, list := range drawData.CommandLists() { - - vertexBuffer, vertexBufferSize := list.VertexBuffer() - gl.BindBuffer(gl.ARRAY_BUFFER, imguiInfo.vboID) - gl.BufferData(gl.ARRAY_BUFFER, vertexBufferSize, vertexBuffer, gl.STREAM_DRAW) - - indexBuffer, indexBufferSize := list.IndexBuffer() - gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, imguiInfo.indexBufID) - gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, indexBufferSize, indexBuffer, gl.STREAM_DRAW) - - for _, cmd := range list.Commands() { - if cmd.HasUserCallback() { - cmd.CallUserCallback(list) - } else { - - gl.BindTexture(gl.TEXTURE_2D, imguiInfo.texID) - clipRect := cmd.ClipRect() - gl.Scissor(int32(clipRect.X), int32(fbHeight)-int32(clipRect.W), int32(clipRect.Z-clipRect.X), int32(clipRect.W-clipRect.Y)) - - gl.DrawElementsBaseVertex(gl.TRIANGLES, int32(cmd.ElementCount()), uint32(drawType), gl.PtrOffset(cmd.IndexOffset()*indexSize), int32(cmd.VertexOffset())) - } - } - } - - //Reset gl state - gl.Disable(gl.BLEND) - gl.Disable(gl.SCISSOR_TEST) - gl.Enable(gl.CULL_FACE) - gl.Enable(gl.DEPTH_TEST) } + +func (g *OurGame) ShouldRun() bool { + return !g.Quitting +} + +func (g *OurGame) GetWindow() *engine.Window { + return g.Win +} + +func (g *OurGame) GetImGUI() nmageimgui.ImguiInfo { + return g.ImGUIInfo +} + +func (g *OurGame) Deinit() { + g.Win.Destroy() +} + +func main() { + + runtime.LockOSThread() + + //Init engine + err := engine.Init() + if err != nil { + logging.ErrLog.Fatalln("Failed to init nMage. Err:", err) + } + + //Create window + window, err = engine.CreateOpenGLWindowCentered("nMage", 1280, 720, engine.WindowFlags_RESIZABLE) + if err != nil { + logging.ErrLog.Fatalln("Failed to create window. Err: ", err) + } + defer window.Destroy() + + engine.SetVSync(false) + + game := &OurGame{ + Win: window, + ImGUIInfo: nmageimgui.NewImGUI(), + } + + engine.Run(game) + return +} + +func updateViewMat() { + targetPos := camPos.Clone().Add(camForward) + viewMat := gglm.LookAt(camPos, targetPos, gglm.NewVec3(0, 1, 0)) + simpleMat.SetUnifMat4("viewMat", &viewMat.Mat4) +} + +var time uint64 = 0 +var name string = "" + +var ambientColor gglm.Vec3 = *gglm.NewVec3(1, 1, 1) +var ambientColorStrength float32 = 0.1 + +var lightPos1 gglm.Vec3 = *gglm.NewVec3(2, 2, 0) +var lightColor1 gglm.Vec3 = *gglm.NewVec3(1, 1, 1) diff --git a/ui/imgui/imgui.go b/ui/imgui/imgui.go new file mode 100755 index 0000000..355e612 --- /dev/null +++ b/ui/imgui/imgui.go @@ -0,0 +1,202 @@ +package nmageimgui + +import ( + "github.com/bloeys/gglm/gglm" + "github.com/bloeys/nmage/asserts" + "github.com/bloeys/nmage/materials" + "github.com/bloeys/nmage/timing" + "github.com/go-gl/gl/v4.1-core/gl" + "github.com/inkyblackness/imgui-go/v4" + "github.com/veandco/go-sdl2/sdl" +) + +type ImguiInfo struct { + ImCtx *imgui.Context + + Mat *materials.Material + vaoID uint32 + vboID uint32 + indexBufID uint32 + texID uint32 +} + +func (i *ImguiInfo) FrameStart(winWidth, winHeight float32) { + + if err := i.ImCtx.SetCurrent(); err != nil { + asserts.T(false, "Setting imgui ctx as current failed. Err: "+err.Error()) + } + + imIO := imgui.CurrentIO() + imIO.SetDisplaySize(imgui.Vec2{X: float32(winWidth), Y: float32(winHeight)}) + + // Setup time step (we don't use SDL_GetTicks() because it is using millisecond resolution) + frequency := sdl.GetPerformanceFrequency() + currentTime := sdl.GetPerformanceCounter() + if timing.ElapsedTime() > 0 { + imIO.SetDeltaTime(float32(currentTime-timing.ElapsedTime()) / float32(frequency)) + } else { + imIO.SetDeltaTime(1.0 / 60.0) + } + + imgui.NewFrame() +} + +func (i *ImguiInfo) Render(winWidth, winHeight float32, fbWidth, fbHeight int32) { + + if err := i.ImCtx.SetCurrent(); err != nil { + asserts.T(false, "Setting imgui ctx as current failed. Err: "+err.Error()) + } + + imgui.Render() + + // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) + if fbWidth <= 0 || fbHeight <= 0 { + return + } + + drawData := imgui.RenderedDrawData() + drawData.ScaleClipRects(imgui.Vec2{ + X: float32(fbWidth) / float32(winWidth), + Y: float32(fbHeight) / float32(winHeight), + }) + + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill + gl.Enable(gl.BLEND) + gl.BlendEquation(gl.FUNC_ADD) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + gl.Disable(gl.CULL_FACE) + gl.Disable(gl.DEPTH_TEST) + gl.Enable(gl.SCISSOR_TEST) + gl.PolygonMode(gl.FRONT_AND_BACK, gl.FILL) + + // Setup viewport, orthographic projection matrix + // Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). + // DisplayMin is typically (0,0) for single viewport apps. + + i.Mat.Bind() + + gl.Uniform1i(gl.GetUniformLocation(i.Mat.ShaderProg.ID, gl.Str("Texture\x00")), 0) + + //PERF: only update the ortho matrix on window resize + orthoMat := gglm.Ortho(0, float32(winWidth), 0, float32(winHeight), 0, 20) + i.Mat.SetUnifMat4("ProjMtx", &orthoMat.Mat4) + gl.BindSampler(0, 0) // Rely on combined texture/sampler state. + + // Recreate the VAO every time + // (This is to easily allow multiple GL contexts. VAO are not shared among GL contexts, and + // we don't track creation/deletion of windows so we don't have an obvious key to use to cache them.) + gl.BindVertexArray(i.vaoID) + gl.BindBuffer(gl.ARRAY_BUFFER, i.vboID) + + vertexSize, vertexOffsetPos, vertexOffsetUv, vertexOffsetCol := imgui.VertexBufferLayout() + i.Mat.EnableAttribute("Position") + i.Mat.EnableAttribute("UV") + i.Mat.EnableAttribute("Color") + gl.VertexAttribPointerWithOffset(uint32(i.Mat.GetAttribLoc("Position")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetPos)) + gl.VertexAttribPointerWithOffset(uint32(i.Mat.GetAttribLoc("UV")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetUv)) + gl.VertexAttribPointerWithOffset(uint32(i.Mat.GetAttribLoc("Color")), 4, gl.UNSIGNED_BYTE, true, int32(vertexSize), uintptr(vertexOffsetCol)) + + indexSize := imgui.IndexBufferLayout() + drawType := gl.UNSIGNED_SHORT + if indexSize == 4 { + drawType = gl.UNSIGNED_INT + } + + // Draw + for _, list := range drawData.CommandLists() { + + vertexBuffer, vertexBufferSize := list.VertexBuffer() + gl.BindBuffer(gl.ARRAY_BUFFER, i.vboID) + gl.BufferData(gl.ARRAY_BUFFER, vertexBufferSize, vertexBuffer, gl.STREAM_DRAW) + + indexBuffer, indexBufferSize := list.IndexBuffer() + gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, i.indexBufID) + gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, indexBufferSize, indexBuffer, gl.STREAM_DRAW) + + for _, cmd := range list.Commands() { + if cmd.HasUserCallback() { + cmd.CallUserCallback(list) + } else { + + gl.BindTexture(gl.TEXTURE_2D, i.texID) + clipRect := cmd.ClipRect() + gl.Scissor(int32(clipRect.X), int32(fbHeight)-int32(clipRect.W), int32(clipRect.Z-clipRect.X), int32(clipRect.W-clipRect.Y)) + + gl.DrawElementsBaseVertex(gl.TRIANGLES, int32(cmd.ElementCount()), uint32(drawType), gl.PtrOffset(cmd.IndexOffset()*indexSize), int32(cmd.VertexOffset())) + } + } + } + + //Reset gl state + gl.Disable(gl.BLEND) + gl.Disable(gl.SCISSOR_TEST) + gl.Enable(gl.CULL_FACE) + gl.Enable(gl.DEPTH_TEST) +} + +func NewImGUI() ImguiInfo { + + imguiInfo := ImguiInfo{ + ImCtx: imgui.CreateContext(nil), + Mat: materials.NewMaterial("ImGUI Mat", "./res/shaders/imgui"), + } + + imIO := imgui.CurrentIO() + imIO.SetBackendFlags(imIO.GetBackendFlags() | imgui.BackendFlagsRendererHasVtxOffset) + + gl.GenVertexArrays(1, &imguiInfo.vaoID) + gl.GenBuffers(1, &imguiInfo.vboID) + gl.GenBuffers(1, &imguiInfo.indexBufID) + gl.GenTextures(1, &imguiInfo.texID) + + // Upload font to gpu + gl.BindTexture(gl.TEXTURE_2D, imguiInfo.texID) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0) + + image := imIO.Fonts().TextureDataAlpha8() + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(image.Width), int32(image.Height), 0, gl.RED, gl.UNSIGNED_BYTE, image.Pixels) + + // Store our identifier + imIO.Fonts().SetTextureID(imgui.TextureID(imguiInfo.texID)) + + //Shader attributes + imguiInfo.Mat.Bind() + imguiInfo.Mat.EnableAttribute("Position") + imguiInfo.Mat.EnableAttribute("UV") + imguiInfo.Mat.EnableAttribute("Color") + imguiInfo.Mat.UnBind() + + //Init imgui input mapping + keys := map[int]int{ + imgui.KeyTab: sdl.SCANCODE_TAB, + imgui.KeyLeftArrow: sdl.SCANCODE_LEFT, + imgui.KeyRightArrow: sdl.SCANCODE_RIGHT, + imgui.KeyUpArrow: sdl.SCANCODE_UP, + imgui.KeyDownArrow: sdl.SCANCODE_DOWN, + imgui.KeyPageUp: sdl.SCANCODE_PAGEUP, + imgui.KeyPageDown: sdl.SCANCODE_PAGEDOWN, + imgui.KeyHome: sdl.SCANCODE_HOME, + imgui.KeyEnd: sdl.SCANCODE_END, + imgui.KeyInsert: sdl.SCANCODE_INSERT, + imgui.KeyDelete: sdl.SCANCODE_DELETE, + imgui.KeyBackspace: sdl.SCANCODE_BACKSPACE, + imgui.KeySpace: sdl.SCANCODE_BACKSPACE, + imgui.KeyEnter: sdl.SCANCODE_RETURN, + imgui.KeyEscape: sdl.SCANCODE_ESCAPE, + imgui.KeyA: sdl.SCANCODE_A, + imgui.KeyC: sdl.SCANCODE_C, + imgui.KeyV: sdl.SCANCODE_V, + imgui.KeyX: sdl.SCANCODE_X, + imgui.KeyY: sdl.SCANCODE_Y, + imgui.KeyZ: sdl.SCANCODE_Z, + } + + // Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array. + for imguiKey, nativeKey := range keys { + imIO.KeyMap(imguiKey, nativeKey) + } + + return imguiInfo +}