package engine import ( "image" "image/color" "runtime" imgui "github.com/AllenDang/cimgui-go" "github.com/bloeys/nmage/assert" "github.com/bloeys/nmage/assets" "github.com/bloeys/nmage/input" "github.com/bloeys/nmage/timing" nmageimgui "github.com/bloeys/nmage/ui/imgui" "github.com/go-gl/gl/v4.1-core/gl" "github.com/veandco/go-sdl2/sdl" ) var ( isInited = false isSdlButtonLeftDown = false isSdlButtonMiddleDown = false isSdlButtonRightDown = false ImguiRelativeMouseModePosX float32 ImguiRelativeMouseModePosY float32 ) type Window struct { SDLWin *sdl.Window GlCtx sdl.GLContext EventCallbacks []func(sdl.Event) } func (w *Window) handleInputs() { imIo := imgui.CurrentIO() imguiCaptureMouse := imIo.WantCaptureMouse() imguiCaptureKeyboard := imIo.WantCaptureKeyboard() input.EventLoopStart(imguiCaptureMouse, imguiCaptureKeyboard) for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { //Fire callbacks for i := 0; i < len(w.EventCallbacks); i++ { w.EventCallbacks[i](event) } //Internal processing switch e := event.(type) { case *sdl.MouseWheelEvent: input.HandleMouseWheelEvent(e) imIo.AddMouseWheelDelta(float32(e.X), float32(e.Y)) case *sdl.KeyboardEvent: input.HandleKeyboardEvent(e) // Send modifier key updates to imgui (based on the imgui SDL backend) imIo.AddKeyEvent(imgui.ModCtrl, e.Keysym.Mod&sdl.KMOD_CTRL != 0) imIo.AddKeyEvent(imgui.ModShift, e.Keysym.Mod&sdl.KMOD_SHIFT != 0) imIo.AddKeyEvent(imgui.ModAlt, e.Keysym.Mod&sdl.KMOD_ALT != 0) imIo.AddKeyEvent(imgui.ModSuper, e.Keysym.Mod&sdl.KMOD_GUI != 0) imIo.AddKeyEvent(nmageimgui.SdlScancodeToImGuiKey(e.Keysym.Scancode), e.Type == sdl.KEYDOWN) case *sdl.TextInputEvent: imIo.AddInputCharactersUTF8(e.GetText()) case *sdl.MouseButtonEvent: input.HandleMouseBtnEvent(e) isPressed := e.State == sdl.PRESSED if e.Button == sdl.BUTTON_LEFT { isSdlButtonLeftDown = isPressed } else if e.Button == sdl.BUTTON_MIDDLE { isSdlButtonMiddleDown = isPressed } else if e.Button == sdl.BUTTON_RIGHT { isSdlButtonRightDown = isPressed } 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 sdl.GetRelativeMouseMode() { imIo.SetMousePos(imgui.Vec2{X: ImguiRelativeMouseModePosX, Y: ImguiRelativeMouseModePosY}) } else { x, y, _ := sdl.GetMouseState() imIo.SetMousePos(imgui.Vec2{X: float32(x), Y: float32(y)}) } // 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. imIo.SetMouseButtonDown(imgui.MouseButtonLeft, isSdlButtonLeftDown) imIo.SetMouseButtonDown(imgui.MouseButtonRight, isSdlButtonRightDown) imIo.SetMouseButtonDown(imgui.MouseButtonMiddle, isSdlButtonMiddleDown) } 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() } func Init() error { isInited = true runtime.LockOSThread() timing.Init() err := initSDL() return err } func initSDL() error { err := sdl.Init(sdl.INIT_TIMER | sdl.INIT_VIDEO) if err != nil { return err } sdl.ShowCursor(1) sdl.GLSetAttribute(sdl.MAJOR_VERSION, 4) sdl.GLSetAttribute(sdl.MINOR_VERSION, 1) sdl.GLSetAttribute(sdl.GL_RED_SIZE, 8) sdl.GLSetAttribute(sdl.GL_GREEN_SIZE, 8) sdl.GLSetAttribute(sdl.GL_BLUE_SIZE, 8) sdl.GLSetAttribute(sdl.GL_ALPHA_SIZE, 8) sdl.GLSetAttribute(sdl.GL_DOUBLEBUFFER, 1) sdl.GLSetAttribute(sdl.GL_DEPTH_SIZE, 24) sdl.GLSetAttribute(sdl.GL_STENCIL_SIZE, 8) sdl.GLSetAttribute(sdl.GL_FRAMEBUFFER_SRGB_CAPABLE, 1) // Allows us to do MSAA sdl.GLSetAttribute(sdl.GL_MULTISAMPLEBUFFERS, 1) sdl.GLSetAttribute(sdl.GL_MULTISAMPLESAMPLES, 4) sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE) return nil } func CreateOpenGLWindow(title string, x, y, width, height int32, flags WindowFlags) (Window, error) { return createWindow(title, x, y, width, height, WindowFlags_OPENGL|flags) } func CreateOpenGLWindowCentered(title string, width, height int32, flags WindowFlags) (Window, error) { return createWindow(title, sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED, width, height, WindowFlags_OPENGL|flags) } func createWindow(title string, x, y, width, height int32, flags WindowFlags) (Window, error) { assert.T(isInited, "engine.Init() was not called!") win := Window{ SDLWin: nil, EventCallbacks: make([]func(sdl.Event), 0), } var err error win.SDLWin, err = sdl.CreateWindow(title, x, y, width, height, uint32(flags)) if err != nil { return win, err } win.GlCtx, err = win.SDLWin.GLCreateContext() if err != nil { return win, err } err = initOpenGL() if err != nil { return win, err } setupDefaultTextures() // Get rid of the blinding white startup screen (unfortunately there is still one frame of white) gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) win.SDLWin.GLSwap() return win, err } func initOpenGL() error { if err := gl.Init(); err != nil { return err } gl.Enable(gl.DEPTH_TEST) gl.Enable(gl.STENCIL_TEST) gl.Enable(gl.CULL_FACE) gl.CullFace(gl.BACK) gl.FrontFace(gl.CCW) gl.Enable(gl.BLEND) gl.Enable(gl.MULTISAMPLE) gl.Enable(gl.FRAMEBUFFER_SRGB) gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) gl.ClearColor(0, 0, 0, 1) return nil } func setupDefaultTextures() error { // 1x1 black texture defaultBlackImg := image.NewNRGBA(image.Rect(0, 0, 1, 1)) defaultBlackImg.Set(0, 0, color.NRGBA{R: 0, G: 0, B: 0, A: 1}) defaultBlackImgTex, err := assets.LoadTextureInMemPngImg(defaultBlackImg, &assets.TextureLoadOptions{NoSrgba: true}) if err != nil { return err } assets.DefaultBlackTexId = defaultBlackImgTex // 1x1 white texture defaultWhiteImg := image.NewNRGBA(image.Rect(0, 0, 1, 1)) defaultWhiteImg.Set(0, 0, color.NRGBA{R: 255, G: 255, B: 255, A: 1}) defaultWhiteImgTex, err := assets.LoadTextureInMemPngImg(defaultWhiteImg, &assets.TextureLoadOptions{NoSrgba: true}) if err != nil { return err } assets.DefaultWhiteTexId = defaultWhiteImgTex // Default diffuse assets.DefaultDiffuseTexId = defaultWhiteImgTex // Default specular assets.DefaultSpecularTexId = defaultBlackImgTex // Default Normal map which is created to be RGB(0.5,0.5,1), which when multiplied by TBN matrix gives the vertex normal. // 128 is better than 127 for normal maps. See 'Flat Color' section here: http://wiki.polycount.com/wiki/Normal_map // Basically, 127 can create seams while 128 looks correct defaultNormalMapImg := image.NewNRGBA(image.Rect(0, 0, 1, 1)) defaultNormalMapImg.Set(0, 0, color.NRGBA{R: 128, G: 128, B: 255, A: 1}) defaultNormalMapTex, err := assets.LoadTextureInMemPngImg(defaultNormalMapImg, &assets.TextureLoadOptions{NoSrgba: true}) if err != nil { return err } assets.DefaultNormalTexId = defaultNormalMapTex // Default emission assets.DefaultEmissionTexId = defaultBlackImgTex assert.T(assets.DefaultBlackTexId.TexID != 0, "The default black texture handle is zero. Either texture wasn't created or handle wasn't updated") assert.T(assets.DefaultWhiteTexId.TexID != 0, "The default white texture handle is zero. Either texture wasn't created or handle wasn't updated") assert.T(assets.DefaultDiffuseTexId.TexID != 0, "The default diffuse texture handle is zero. Either texture wasn't created or handle wasn't updated") assert.T(assets.DefaultSpecularTexId.TexID != 0, "The default specular texture handle is zero. Either texture wasn't created or handle wasn't updated") assert.T(assets.DefaultNormalTexId.TexID != 0, "The default normal texture handle is zero. Either texture wasn't created or handle wasn't updated") assert.T(assets.DefaultEmissionTexId.TexID != 0, "The default emission texture handle is zero. Either texture wasn't created or handle wasn't updated") return nil } func SetSrgbFramebuffer(isEnabled bool) { if isEnabled { gl.Enable(gl.FRAMEBUFFER_SRGB) } else { gl.Disable(gl.FRAMEBUFFER_SRGB) } } func SetVSync(enabled bool) { if enabled { sdl.GLSetSwapInterval(1) } else { sdl.GLSetSwapInterval(0) } } func SetMSAA(isEnabled bool) { if isEnabled { gl.Enable(gl.MULTISAMPLE) } else { gl.Disable(gl.MULTISAMPLE) } }