|
|
@@ -0,0 +1,475 @@
|
|
|
+package application
|
|
|
+
|
|
|
+import (
|
|
|
+ "flag"
|
|
|
+ "fmt"
|
|
|
+ "os"
|
|
|
+ "runtime"
|
|
|
+ "runtime/pprof"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/g3n/engine/audio/al"
|
|
|
+ "github.com/g3n/engine/audio/ov"
|
|
|
+ "github.com/g3n/engine/audio/vorbis"
|
|
|
+ "github.com/g3n/engine/camera"
|
|
|
+ "github.com/g3n/engine/camera/control"
|
|
|
+ "github.com/g3n/engine/core"
|
|
|
+ "github.com/g3n/engine/gls"
|
|
|
+ "github.com/g3n/engine/gui"
|
|
|
+ "github.com/g3n/engine/math32"
|
|
|
+ "github.com/g3n/engine/renderer"
|
|
|
+ "github.com/g3n/engine/util/logger"
|
|
|
+ "github.com/g3n/engine/window"
|
|
|
+)
|
|
|
+
|
|
|
+// Application is a basic standard application object which can be extended.
|
|
|
+// It creates a Window, OpenGL state, default cameras, default scene and Gui and
|
|
|
+// has a method to run the render loop.
|
|
|
+type Application struct {
|
|
|
+ core.Dispatcher // Embedded event dispatcher
|
|
|
+ core.TimerManager // Embedded timer manager
|
|
|
+ win window.IWindow // Application window
|
|
|
+ gl *gls.GLS // OpenGL state
|
|
|
+ log *logger.Logger // Default application logger
|
|
|
+ renderer *renderer.Renderer // Renderer object
|
|
|
+ camPersp *camera.Perspective // Perspective camera
|
|
|
+ camOrtho *camera.Orthographic // Orthographic camera
|
|
|
+ camera camera.ICamera // Current camera
|
|
|
+ orbit *control.OrbitControl // Camera orbit controller
|
|
|
+ audio bool // Audio available
|
|
|
+ vorbis bool // Vorbis decoder available
|
|
|
+ audioEFX bool // Audio effect extension support available
|
|
|
+ audioDev *al.Device // Audio player device
|
|
|
+ captureDev *al.Device // Audio capture device
|
|
|
+ frameRater *FrameRater // Render loop frame rater
|
|
|
+ scene *core.Node // Node container for 3D tests
|
|
|
+ guiroot *gui.Root // Gui root panel
|
|
|
+ frameCount uint64 // Frame counter
|
|
|
+ frameTime time.Time // Time at the start of the frame
|
|
|
+ frameDelta time.Duration // Time delta from previous frame
|
|
|
+ startTime time.Time // Time at the start of the render loop
|
|
|
+ cpuProfile string // File to write cpu profile to
|
|
|
+ swapInterval int // Swap interval option
|
|
|
+ targetFPS uint // Target FPS option
|
|
|
+ noglErrors bool // No OpenGL check errors options
|
|
|
+}
|
|
|
+
|
|
|
+// Options defines initial options passed to application creation function
|
|
|
+type Options struct {
|
|
|
+ WinHeight int // Initial window height (default is screen width)
|
|
|
+ WinWidth int // Initial window width (default is screen height)
|
|
|
+ LogLevel int // Initial log level (default = DEBUG)
|
|
|
+ EnableFlags bool // Enable command line flags (default = false)
|
|
|
+ TargetFPS uint // Desired frames per second rate (default = 60)
|
|
|
+}
|
|
|
+
|
|
|
+// appInstance contains the pointer to the single Application instance
|
|
|
+var appInstance *Application
|
|
|
+
|
|
|
+const (
|
|
|
+ OnBeforeRender = "util.application.OnBeforeRender"
|
|
|
+ OnAfterRender = "util.application.OnAfterRender"
|
|
|
+)
|
|
|
+
|
|
|
+// Creates creates and returns the application object using the specified name for
|
|
|
+// the window title and log messages
|
|
|
+// This function must be called only once.
|
|
|
+func Create(name string, ops Options) (*Application, error) {
|
|
|
+
|
|
|
+ if appInstance != nil {
|
|
|
+ return nil, fmt.Errorf("Application already created")
|
|
|
+ }
|
|
|
+ app := new(Application)
|
|
|
+ app.Dispatcher.Initialize()
|
|
|
+ app.TimerManager.Initialize()
|
|
|
+ appInstance = app
|
|
|
+
|
|
|
+ // Initialize options default values
|
|
|
+ app.cpuProfile = ""
|
|
|
+ app.swapInterval = -1
|
|
|
+ app.targetFPS = 60
|
|
|
+ if ops.TargetFPS != 0 {
|
|
|
+ app.targetFPS = ops.TargetFPS
|
|
|
+ }
|
|
|
+ app.noglErrors = false
|
|
|
+
|
|
|
+ // Parse standard application command flags if requested
|
|
|
+ if ops.EnableFlags {
|
|
|
+ cpuProfile := flag.String("cpuprofile", "", "Activate cpu profiling writing profile to the specified file")
|
|
|
+ swapInterval := flag.Int("swapinterval", -1, "Sets the swap buffers interval to this value")
|
|
|
+ targetFPS := flag.Uint("targetfps", 60, "Sets the frame rate in frames per second")
|
|
|
+ noglErrors := flag.Bool("noglerrors", false, "Do not check OpenGL errors at each call (may increase FPS)")
|
|
|
+ flag.Parse()
|
|
|
+ app.cpuProfile = *cpuProfile
|
|
|
+ app.swapInterval = *swapInterval
|
|
|
+ app.targetFPS = *targetFPS
|
|
|
+ app.noglErrors = *noglErrors
|
|
|
+ }
|
|
|
+
|
|
|
+ // Creates application logger
|
|
|
+ app.log = logger.New(name, nil)
|
|
|
+ app.log.AddWriter(logger.NewConsole(false))
|
|
|
+ app.log.SetFormat(logger.FTIME | logger.FMICROS)
|
|
|
+ app.log.SetLevel(ops.LogLevel)
|
|
|
+
|
|
|
+ // Window event handling must run on the main OS thread
|
|
|
+ runtime.LockOSThread()
|
|
|
+
|
|
|
+ // Creates window and sets it as the current context
|
|
|
+ win, err := window.New("glfw", 10, 10, name, false)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ // Sets the window size
|
|
|
+ swidth, sheight := win.GetScreenResolution(nil)
|
|
|
+ if ops.WinWidth != 0 {
|
|
|
+ swidth = ops.WinWidth
|
|
|
+ }
|
|
|
+ if ops.WinHeight != 0 {
|
|
|
+ sheight = ops.WinHeight
|
|
|
+ }
|
|
|
+ win.SetSize(swidth, sheight)
|
|
|
+ app.win = win
|
|
|
+
|
|
|
+ // Create OpenGL state
|
|
|
+ gl, err := gls.New()
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ app.gl = gl
|
|
|
+ app.gl.SetCheckErrors(!app.noglErrors)
|
|
|
+ cc := math32.NewColor("gray")
|
|
|
+ app.gl.ClearColor(cc.R, cc.G, cc.B, 1)
|
|
|
+ app.gl.Clear(gls.DEPTH_BUFFER_BIT | gls.STENCIL_BUFFER_BIT | gls.COLOR_BUFFER_BIT)
|
|
|
+
|
|
|
+ // Creates perspective camera
|
|
|
+ width, height := app.win.GetSize()
|
|
|
+ aspect := float32(width) / float32(height)
|
|
|
+ app.camPersp = camera.NewPerspective(65, aspect, 0.01, 1000)
|
|
|
+
|
|
|
+ // Creates orthographic camera
|
|
|
+ app.camOrtho = camera.NewOrthographic(-2, 2, 2, -2, 0.01, 100)
|
|
|
+ app.camOrtho.SetPosition(0, 0, 3)
|
|
|
+ app.camOrtho.LookAt(&math32.Vector3{0, 0, 0})
|
|
|
+ app.camOrtho.SetZoom(1.0)
|
|
|
+
|
|
|
+ // Default camera is perspective
|
|
|
+ app.camera = app.camPersp
|
|
|
+
|
|
|
+ // Creates orbit camera control
|
|
|
+ // It is important to do this after the root panel subscription
|
|
|
+ // to avoid GUI events being propagated to the orbit control.
|
|
|
+ app.orbit = control.NewOrbitControl(app.camera, app.win)
|
|
|
+
|
|
|
+ // Creates scene for 3D objects
|
|
|
+ app.scene = core.NewNode()
|
|
|
+
|
|
|
+ // Creates gui root panel
|
|
|
+ app.guiroot = gui.NewRoot(app.gl, app.win)
|
|
|
+
|
|
|
+ // Creates renderer
|
|
|
+ app.renderer = renderer.NewRenderer(gl)
|
|
|
+ err = app.renderer.AddDefaultShaders()
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("Error from AddDefaulShaders:%v", err)
|
|
|
+ }
|
|
|
+ app.renderer.SetScene(app.scene)
|
|
|
+ app.renderer.SetGui(app.guiroot)
|
|
|
+
|
|
|
+ // Create frame rater
|
|
|
+ app.frameRater = NewFrameRater(uint(app.targetFPS))
|
|
|
+
|
|
|
+ return app, nil
|
|
|
+}
|
|
|
+
|
|
|
+// App returns the application single instance or nil
|
|
|
+// if the application was not created yet
|
|
|
+func Get() *Application {
|
|
|
+
|
|
|
+ return appInstance
|
|
|
+}
|
|
|
+
|
|
|
+// Log returns the application logger
|
|
|
+func (app *Application) Log() *logger.Logger {
|
|
|
+
|
|
|
+ return app.log
|
|
|
+}
|
|
|
+
|
|
|
+// Window returns the application window
|
|
|
+func (app *Application) Window() window.IWindow {
|
|
|
+
|
|
|
+ return app.win
|
|
|
+}
|
|
|
+
|
|
|
+// Gl returns the OpenGL state associated
|
|
|
+func (app *Application) Gl() *gls.GLS {
|
|
|
+
|
|
|
+ return app.gl
|
|
|
+}
|
|
|
+
|
|
|
+// Gui returns the current application Gui root panel
|
|
|
+func (app *Application) Gui() *gui.Root {
|
|
|
+
|
|
|
+ return app.guiroot
|
|
|
+}
|
|
|
+
|
|
|
+// Scene returns the current application 3D scene
|
|
|
+func (app *Application) Scene() *core.Node {
|
|
|
+
|
|
|
+ return app.scene
|
|
|
+}
|
|
|
+
|
|
|
+// SetScene sets the 3D scene to be rendered
|
|
|
+func (app *Application) SetScene(scene *core.Node) {
|
|
|
+
|
|
|
+ app.renderer.SetScene(scene)
|
|
|
+}
|
|
|
+
|
|
|
+// SetGui sets the root panel of the gui to be rendered
|
|
|
+func (app *Application) SetGui(root *gui.Root) {
|
|
|
+
|
|
|
+ app.guiroot = root
|
|
|
+ app.renderer.SetGui(app.guiroot)
|
|
|
+}
|
|
|
+
|
|
|
+// SetPanel3D sets the gui panel inside which the 3D scene is shown.
|
|
|
+func (app *Application) SetPanel3D(panel3D gui.IPanel) {
|
|
|
+
|
|
|
+ app.renderer.SetGuiPanel3D(panel3D)
|
|
|
+}
|
|
|
+
|
|
|
+// Panel3D returns the current gui panel where the 3D scene is shown.
|
|
|
+func (app *Application) Panel3D() gui.IPanel {
|
|
|
+
|
|
|
+ return app.renderer.Panel3D()
|
|
|
+}
|
|
|
+
|
|
|
+// CameraPersp returns the application perspective camera
|
|
|
+func (app *Application) CameraPersp() *camera.Perspective {
|
|
|
+
|
|
|
+ return app.camPersp
|
|
|
+}
|
|
|
+
|
|
|
+// CameraOrtho returns the application orthographic camera
|
|
|
+func (app *Application) CameraOrtho() *camera.Orthographic {
|
|
|
+
|
|
|
+ return app.camOrtho
|
|
|
+}
|
|
|
+
|
|
|
+// Camera returns the current application camera
|
|
|
+func (app *Application) Camera() camera.ICamera {
|
|
|
+
|
|
|
+ return app.camera
|
|
|
+}
|
|
|
+
|
|
|
+// SetCamera sets the current application camera
|
|
|
+func (app *Application) SetCamera(cam camera.ICamera) {
|
|
|
+
|
|
|
+ app.camera = cam
|
|
|
+}
|
|
|
+
|
|
|
+// Orbit returns the current camera orbit control
|
|
|
+func (app *Application) Orbit() *control.OrbitControl {
|
|
|
+
|
|
|
+ return app.orbit
|
|
|
+}
|
|
|
+
|
|
|
+// SetOrbit sets the camera orbit control
|
|
|
+func (app *Application) SetOrbit(oc *control.OrbitControl) {
|
|
|
+
|
|
|
+ app.orbit = oc
|
|
|
+}
|
|
|
+
|
|
|
+// FrameRater returns the FrameRater object
|
|
|
+func (app *Application) FrameRater() *FrameRater {
|
|
|
+
|
|
|
+ return app.frameRater
|
|
|
+}
|
|
|
+
|
|
|
+func (app *Application) FrameCount() uint64 {
|
|
|
+
|
|
|
+ return app.frameCount
|
|
|
+}
|
|
|
+
|
|
|
+func (app *Application) FrameDelta() time.Duration {
|
|
|
+
|
|
|
+ return app.frameDelta
|
|
|
+}
|
|
|
+
|
|
|
+func (app *Application) FrameDeltaSeconds() float32 {
|
|
|
+
|
|
|
+ return float32(app.frameDelta.Seconds())
|
|
|
+}
|
|
|
+
|
|
|
+func (app *Application) RunTime() time.Duration {
|
|
|
+
|
|
|
+ return time.Now().Sub(app.startTime)
|
|
|
+}
|
|
|
+
|
|
|
+func (app *Application) RunSeconds() float32 {
|
|
|
+
|
|
|
+ return float32(time.Now().Sub(app.startTime).Seconds())
|
|
|
+}
|
|
|
+
|
|
|
+func (app *Application) Renderer() *renderer.Renderer {
|
|
|
+
|
|
|
+ return app.renderer
|
|
|
+}
|
|
|
+
|
|
|
+func (app *Application) AudioSupport() bool {
|
|
|
+
|
|
|
+ return app.audio
|
|
|
+}
|
|
|
+
|
|
|
+func (app *Application) VorbisSupport() bool {
|
|
|
+
|
|
|
+ return app.vorbis
|
|
|
+}
|
|
|
+
|
|
|
+// SetCpuProfile must be called before Run() and sets the file name for cpu profiling.
|
|
|
+// If set the cpu profiling starts before running the render loop and continues
|
|
|
+// till the end of the application.
|
|
|
+func (app *Application) SetCpuProfile(fname string) {
|
|
|
+
|
|
|
+ app.cpuProfile = fname
|
|
|
+}
|
|
|
+
|
|
|
+// Runs runs the application render loop
|
|
|
+func (app *Application) Run() error {
|
|
|
+
|
|
|
+ // Set swap interval
|
|
|
+ if app.swapInterval >= 0 {
|
|
|
+ app.win.SwapInterval(app.swapInterval)
|
|
|
+ app.log.Debug("Swap interval set to:%v", app.swapInterval)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Start profiling if requested
|
|
|
+ if app.cpuProfile != "" {
|
|
|
+ f, err := os.Create(app.cpuProfile)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ err = pprof.StartCPUProfile(f)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ app.log.Info("Started writing CPU profile to:%s", app.cpuProfile)
|
|
|
+ defer pprof.StopCPUProfile()
|
|
|
+ }
|
|
|
+
|
|
|
+ app.startTime = time.Now()
|
|
|
+ app.frameTime = time.Now()
|
|
|
+
|
|
|
+ // Render loop
|
|
|
+ for !app.win.ShouldClose() {
|
|
|
+ // Starts measuring this frame
|
|
|
+ app.frameRater.Start()
|
|
|
+
|
|
|
+ // Updates frame start and time delta in context
|
|
|
+ now := time.Now()
|
|
|
+ app.frameDelta = now.Sub(app.frameTime)
|
|
|
+ app.frameTime = now
|
|
|
+
|
|
|
+ // Process root panel timers
|
|
|
+ if app.Gui() != nil {
|
|
|
+ app.Gui().TimerManager.ProcessTimers()
|
|
|
+ }
|
|
|
+
|
|
|
+ // Process application timers
|
|
|
+ app.ProcessTimers()
|
|
|
+
|
|
|
+ // Dispatch before render event
|
|
|
+ app.Dispatch(OnBeforeRender, nil)
|
|
|
+
|
|
|
+ // Renders the current scene and/or gui
|
|
|
+ rendered, err := app.renderer.Render(app.camera)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Poll input events and process them
|
|
|
+ app.win.PollEvents()
|
|
|
+
|
|
|
+ if rendered {
|
|
|
+ app.win.SwapBuffers()
|
|
|
+ }
|
|
|
+
|
|
|
+ // Dispatch after render event
|
|
|
+ app.Dispatch(OnAfterRender, nil)
|
|
|
+
|
|
|
+ // Controls the frame rate and updates the FPS for the user
|
|
|
+ app.frameRater.Wait()
|
|
|
+ app.frameCount++
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// Quit ends the application
|
|
|
+func (app *Application) Quit() {
|
|
|
+
|
|
|
+ app.win.SetShouldClose(true)
|
|
|
+}
|
|
|
+
|
|
|
+// OnWindowResize is default handler for window resize events.
|
|
|
+func (app *Application) OnWindowResize(evname string, ev interface{}) {
|
|
|
+
|
|
|
+ // Get window size and sets the viewport to the same size
|
|
|
+ width, height := app.win.GetSize()
|
|
|
+ app.gl.Viewport(0, 0, int32(width), int32(height))
|
|
|
+
|
|
|
+ // Sets perspective camera aspect ratio
|
|
|
+ aspect := float32(width) / float32(height)
|
|
|
+ app.camPersp.SetAspect(aspect)
|
|
|
+
|
|
|
+ // Sets the size of GUI root panel size to the size of the screen
|
|
|
+ if app.guiroot != nil {
|
|
|
+ app.guiroot.SetSize(float32(width), float32(height))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// LoadAudioLibs try to load audio libraries
|
|
|
+func (app *Application) LoadAudioLibs() error {
|
|
|
+
|
|
|
+ // Try to load OpenAL
|
|
|
+ err := al.Load()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Opens default audio device
|
|
|
+ app.audioDev, err = al.OpenDevice("")
|
|
|
+ if app.audioDev == nil {
|
|
|
+ return fmt.Errorf("Error: %s opening OpenAL default device", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Checks for OpenAL effects extension support
|
|
|
+ if al.IsExtensionPresent("ALC_EXT_EFX") {
|
|
|
+ app.audioEFX = true
|
|
|
+ }
|
|
|
+
|
|
|
+ // Creates audio context with auxiliary sends
|
|
|
+ var attribs []int
|
|
|
+ if app.audioEFX {
|
|
|
+ attribs = []int{al.MAX_AUXILIARY_SENDS, 4}
|
|
|
+ }
|
|
|
+ acx, err := al.CreateContext(app.audioDev, attribs)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("Error creating audio context:%s", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Makes the context the current one
|
|
|
+ err = al.MakeContextCurrent(acx)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("Error setting audio context current:%s", err)
|
|
|
+ }
|
|
|
+ app.audio = true
|
|
|
+ app.log.Info("%s version: %s", al.GetString(al.Vendor), al.GetString(al.Version))
|
|
|
+
|
|
|
+ // Ogg Vorbis support
|
|
|
+ err = ov.Load()
|
|
|
+ if err == nil {
|
|
|
+ app.vorbis = true
|
|
|
+ vorbis.Load()
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|