package main import ( "fmt" "math" "os" "runtime/pprof" "github.com/bloeys/gglm/gglm" "github.com/bloeys/nmage/engine" "github.com/bloeys/nmage/input" "github.com/bloeys/nmage/materials" "github.com/bloeys/nmage/meshes" "github.com/bloeys/nmage/renderer/rend3dgl" "github.com/bloeys/nmage/timing" nmageimgui "github.com/bloeys/nmage/ui/imgui" "github.com/bloeys/nterm/assert" "github.com/bloeys/nterm/consts" "github.com/bloeys/nterm/glyphs" "github.com/golang/freetype/truetype" "github.com/inkyblackness/imgui-go/v4" "github.com/veandco/go-sdl2/sdl" "golang.org/x/exp/constraints" "golang.org/x/image/font" ) var _ engine.Game = &program{} type program struct { win *engine.Window rend *rend3dgl.Rend3DGL imguiInfo nmageimgui.ImguiInfo FontSize uint32 Dpi float64 GlyphRend *glyphs.GlyphRend gridMesh *meshes.Mesh gridMat *materials.Material textBuf []rune textBufSize int64 textBufHead int64 cursorPos *gglm.Vec3 } const ( subPixelX = 64 subPixelY = 64 hinting = font.HintingNone defaultTextBufSize = 4 * 1024 * 1024 ) var ( isDrawingBounds = false drawManyLines = false drawGrid bool textToShow = "" textColor = gglm.NewVec4(1, 1, 1, 1) xOff float32 = 0 yOff float32 = 0 ) func main() { err := engine.Init() if err != nil { panic("Failed to init engine. Err: " + err.Error()) } rend := rend3dgl.NewRend3DGL() win, err := engine.CreateOpenGLWindowCentered("nTerm", 1280, 720, engine.WindowFlags_ALLOW_HIGHDPI|engine.WindowFlags_RESIZABLE, rend) if err != nil { panic("Failed to create window. Err: " + err.Error()) } engine.SetVSync(true) p := &program{ win: win, rend: rend, imguiInfo: nmageimgui.NewImGUI(), FontSize: 40, textBuf: make([]rune, defaultTextBufSize), textBufSize: defaultTextBufSize, textBufHead: 0, cursorPos: gglm.NewVec3(0, 0, 0), } p.win.EventCallbacks = append(p.win.EventCallbacks, p.handleSDLEvent) //Don't flash white p.win.SDLWin.GLSwap() if consts.Mode_Debug { var pf, _ = os.Create("pprof.cpu") defer pf.Close() pprof.StartCPUProfile(pf) } engine.Run(p, p.win, p.imguiInfo) if consts.Mode_Debug { pprof.StopCPUProfile() } } func (p *program) handleSDLEvent(e sdl.Event) { switch e := e.(type) { case *sdl.TextInputEvent: p.WriteToBuf([]rune(e.GetText())) case *sdl.WindowEvent: if e.Event == sdl.WINDOWEVENT_SIZE_CHANGED { p.HandleWindowResize() } } } func (p *program) Init() { dpi, _, _, err := sdl.GetDisplayDPI(0) if err != nil { panic("Failed to get display DPI. Err: " + err.Error()) } fmt.Printf("DPI: %f, font size: %d\n", dpi, p.FontSize) w, h := p.win.SDLWin.GetSize() // p.GlyphRend, err = glyphs.NewGlyphRend("./res/fonts/tajawal-regular-var.ttf", &truetype.Options{Size: float64(p.FontSize), DPI: p.Dpi, SubPixelsX: subPixelX, SubPixelsY: subPixelY, Hinting: hinting}, w, h) p.GlyphRend, err = glyphs.NewGlyphRend("./res/fonts/alm-fixed.ttf", &truetype.Options{Size: float64(p.FontSize), DPI: p.Dpi, SubPixelsX: subPixelX, SubPixelsY: subPixelY, Hinting: hinting}, w, h) if err != nil { panic("Failed to create atlas from font file. Err: " + err.Error()) } if consts.Mode_Debug { glyphs.SaveImgToPNG(p.GlyphRend.Atlas.Img, "./debug-atlas.png") } //Load resources p.gridMesh, err = meshes.NewMesh("grid", "./res/models/quad.obj", 0) if err != nil { panic(err.Error()) } p.gridMat = materials.NewMaterial("grid", "./res/shaders/grid.glsl") p.HandleWindowResize() //Set initial cursor pos p.cursorPos.SetY(p.GlyphRend.Atlas.LineHeight) } func (p *program) Update() { if input.IsQuitClicked() || input.KeyClicked(sdl.K_ESCAPE) { engine.Quit() } if consts.Mode_Debug { p.DebugUpdate() } //Font sizing oldFont := p.FontSize fontSizeChanged := false if input.KeyClicked(sdl.K_KP_PLUS) { p.FontSize += 2 fontSizeChanged = true } else if input.KeyClicked(sdl.K_KP_MINUS) { p.FontSize -= 2 fontSizeChanged = true } if fontSizeChanged { err := p.GlyphRend.SetFace(&truetype.Options{Size: float64(p.FontSize), DPI: p.Dpi, SubPixelsX: subPixelX, SubPixelsY: subPixelY, Hinting: hinting}) if err != nil { p.FontSize = oldFont println("Failed to update font face. Err: " + err.Error()) } else { glyphs.SaveImgToPNG(p.GlyphRend.Atlas.Img, "./debug-atlas.png") println("New font size:", p.FontSize, "; New texture size:", p.GlyphRend.Atlas.Img.Rect.Max.X, "\n") } } p.MainUpdate() } func (p *program) WriteToBuf(text []rune) { newHeadPos := p.textBufHead + int64(len(text)) if newHeadPos <= p.textBufSize { copy(p.textBuf[p.textBufHead:], text) p.textBufHead = newHeadPos return } assert.T(false, "Circular buffer not implemented") } func (p *program) MainUpdate() { if input.KeyClicked(sdl.K_RETURN) || input.KeyClicked(sdl.K_KP_ENTER) { p.WriteToBuf([]rune{'\n'}) } // @TODO: Implement hold to delete if input.KeyClicked(sdl.K_BACKSPACE) { p.textBufHead = clamp(p.textBufHead-1, 0, p.textBufSize) } p.cursorPos.Data = p.GlyphRend.DrawTextOpenGLAbs(p.textBuf[:p.textBufHead], gglm.NewVec3(0, float32(p.GlyphRend.ScreenHeight)-p.GlyphRend.Atlas.LineHeight, 0), gglm.NewVec4(1, 1, 1, 1)).Data p.DrawCursor() } func (p *program) DrawCursor() { p.ScreenPosToGridPos(p.cursorPos) p.GlyphRend.DrawTextOpenGLAbs([]rune{'|'}, p.cursorPos, gglm.NewVec4(1, 1, 1, 1)) } func (p *program) ScreenPosToGridPos(screenPos *gglm.Vec3) { screenPos.SetX(screenPos.X() / p.GlyphRend.Atlas.SpaceAdvance * p.GlyphRend.Atlas.SpaceAdvance) screenPos.SetY(screenPos.Y() / p.GlyphRend.Atlas.LineHeight * p.GlyphRend.Atlas.LineHeight) } func (p *program) DebugUpdate() { //Move text var speed float32 = 1 if input.KeyDown(sdl.K_RIGHT) { xOff += speed } else if input.KeyDown(sdl.K_LEFT) { xOff -= speed } if input.KeyDown(sdl.K_UP) { yOff += speed } else if input.KeyDown(sdl.K_DOWN) { yOff -= speed } //Grid if input.KeyDown(sdl.K_LCTRL) && input.KeyClicked(sdl.K_SPACE) { drawGrid = !drawGrid } //UI imgui.InputText("", &textToShow) if imgui.Button("Print Runs") { runs := make([]glyphs.TextRun, 0, 20) p.GlyphRend.GetTextRuns([]rune(textToShow), &runs) for _, run := range runs { fmt.Printf("%s; runes: %#x\n\n", string(run.Runes), run.Runes) } fmt.Printf("----------------\n") } if imgui.Checkbox("Draw Bounds", &isDrawingBounds) { if isDrawingBounds { p.GlyphRend.GlyphMat.SetUnifInt32("drawBounds", 1) } else { p.GlyphRend.GlyphMat.SetUnifInt32("drawBounds", 0) } } imgui.Checkbox("Draw many", &drawManyLines) glyphs.PrintPositions = imgui.Button("Print positions") } func (p *program) Render() { defer p.GlyphRend.Draw() if consts.Mode_Debug { p.DebugRender() } } func (p *program) DebugRender() { if drawGrid { p.DrawGrid() } str := textToShow charCount := len([]rune(str)) fps := int(timing.GetAvgFPS()) if drawManyLines { const charsPerFrame = 500_000 for i := 0; i < charsPerFrame/charCount; i++ { p.GlyphRend.DrawTextOpenGLAbsString(str, gglm.NewVec3(xOff, float32(p.GlyphRend.Atlas.LineHeight)*5+yOff, 0), textColor) } p.win.SDLWin.SetTitle(fmt.Sprint("FPS: ", fps, " Draws/f: ", math.Ceil(charsPerFrame/glyphs.MaxGlyphsPerBatch), " chars/f: ", charsPerFrame, " chars/s: ", fps*charsPerFrame)) } else { charsPerFrame := float64(charCount) p.GlyphRend.DrawTextOpenGLAbsString(str, gglm.NewVec3(xOff, float32(p.GlyphRend.Atlas.LineHeight)*5+yOff, 0), textColor) p.win.SDLWin.SetTitle(fmt.Sprint("FPS: ", fps, " Draws/f: ", math.Ceil(charsPerFrame/glyphs.MaxGlyphsPerBatch), " chars/f: ", int(charsPerFrame), " chars/s: ", fps*int(charsPerFrame))) } } func (p *program) DrawGrid() { sizeX := float32(p.GlyphRend.ScreenWidth) sizeY := float32(p.GlyphRend.ScreenHeight) //columns adv := p.GlyphRend.Atlas.SpaceAdvance for i := 0; i < int(p.GlyphRend.ScreenWidth); i++ { p.rend.Draw(p.gridMesh, gglm.NewTrMatId().Translate(gglm.NewVec3(adv*float32(i), sizeY/2, 0)).Scale(gglm.NewVec3(1, sizeY, 1)), p.gridMat) } //rows for i := int32(0); i < p.GlyphRend.ScreenHeight; i += int32(p.GlyphRend.Atlas.LineHeight) { p.rend.Draw(p.gridMesh, gglm.NewTrMatId().Translate(gglm.NewVec3(sizeX/2, float32(i), 0)).Scale(gglm.NewVec3(sizeX, 1, 1)), p.gridMat) } } func (p *program) FrameEnd() { } func (p *program) DeInit() { } func (p *program) HandleWindowResize() { w, h := p.win.SDLWin.GetSize() p.GlyphRend.SetScreenSize(w, h) projMtx := gglm.Ortho(0, float32(w), float32(h), 0, 0.1, 20) viewMtx := gglm.LookAt(gglm.NewVec3(0, 0, -10), gglm.NewVec3(0, 0, 0), gglm.NewVec3(0, 1, 0)) p.gridMat.SetUnifMat4("projViewMat", &projMtx.Mul(viewMtx).Mat4) } func FloorF32(x float32) float32 { return float32(math.Floor(float64(x))) } func clamp[T constraints.Ordered](x, min, max T) T { if x < min { return min } if x > max { return max } return x }