├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── behaviour ├── closeable.go └── controllers │ └── camera.go ├── config.example.json ├── container.go ├── engine ├── engine.go ├── imanager.go └── manager.go ├── event └── manager.go ├── go.mod ├── go.sum ├── input ├── keyboard.go ├── keyboard │ └── keys.go ├── manager.go └── mouse.go ├── internal ├── config │ ├── config.go │ └── config_test.go └── debug │ ├── log.go │ └── profiler.go ├── main.go ├── messages ├── keydown.go ├── keyheld.go ├── keyreleased.go └── mousemove.go ├── renderer ├── cache │ └── prop.go ├── camera │ └── frustum.go ├── gl │ ├── bsp │ │ └── cache.go │ ├── material │ │ ├── cache.go │ │ └── types.go │ ├── prop │ │ └── cache.go │ ├── renderer.go │ └── shaders │ │ ├── fragment.go │ │ ├── sky │ │ ├── fragment.go │ │ └── vertex.go │ │ └── vertex.go ├── irenderer.go ├── manager.go └── ui │ ├── imgui-go_impl3.go │ └── renderer.go ├── renovate.json ├── scene ├── loader.go ├── scene.go ├── visibility │ ├── cache.go │ └── vis.go └── world │ ├── sky.go │ └── world.go ├── ui ├── dialogs │ └── syscalls.go └── ui.go └── window └── manager.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: galaco/go-gtk-ci:1.13b1 6 | steps: 7 | - checkout 8 | - run: 9 | name: Install Go Dependencies 10 | command: | 11 | go get -u 12 | - run: 13 | name: Lint 14 | command: golangci-lint run --deadline=2m 15 | - run: 16 | name: Test 17 | command: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 18 | - run: 19 | name: Upload Codecov Results 20 | command: bash <(curl -s https://codecov.io/bash) 21 | when: on_success 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \.DS_Store 2 | vendor 3 | \.idea/ 4 | 5 | config\.json 6 | example_game/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lambda Client 2 | > Lambda Client is a game engine written in golang designed that loads Valve's Source Engine projects. 3 | 4 | **This project has been deprecated (for now) in favour of [https://github.com/galaco/kero](https://github.com/galaco/kero)** 5 | 6 | [![CircleCI](https://circleci.com/gh/Galaco/lambda-client.svg?style=svg)](https://circleci.com/gh/Galaco/lambda-client) 7 | [![GoDoc](https://godoc.org/github.com/Galaco/lambda-client?status.svg)](https://godoc.org/github.com/Galaco/lambda-client) 8 | [![Go report card](https://goreportcard.com/badge/github.com/galaco/lambda-client)](https://goreportcard.com/report/github.com/galaco/lambda-client) 9 | [![GolangCI](https://golangci.com/badges/github.com/galaco/lambda-client.svg)](https://golangci.com/r/github.com/Galaco/lambda-client) 10 | [![codecov](https://codecov.io/gh/Galaco/lambda-client/branch/master/graph/badge.svg)](https://codecov.io/gh/Galaco/lambda-client) 11 | 12 | The end goal is to be able to point this application at a source engine game and be able to 13 | load and play that games levels. Where this progresses beyond that, needs to be decided. Most likely this will be come either a thin client for multiple 14 | source games with game specific code layered on top (target multiplayer as priority), or the full server simulation for single player games 15 | would be written (targeting single player as priority). 16 | 17 | ![de_dust2](https://cdn.galaco.me/github/lambda-client/readme/de_dust2_optimised.gif) 18 | 19 | ## Features 20 | You can build this right now, and, assuming you set the configuration to point to an existing Source game installation (this is tested primarily against CS:S): 21 | * Loads game data files from projects gameinfo.txt 22 | * Load BSP maps 23 | * Load high-resolution texture data for bsp faces, including pakfile entries 24 | * Full visibility data support 25 | * Staticprop loading (working, but is incomplete) 26 | * Basic entdata loading (dynamic and physics props) 27 | 28 | ## Installation 29 | Windows, Mac & Linux are all supported. 30 | 31 | There is a small amount of configuration required to get this project running, beyond `go get`. 32 | * For best results, you need a source engine game installed already. 33 | * Copy `config.example.json` to `config.json`, and update the `gameDirectory` property to point to whatever game installation 34 | you are targeting (e.g. HL2 would be `/steamapps/common/hl2`). 35 | 36 | ## Contributing 37 | 1. Fork it () 38 | 2. Create your feature branch (`git checkout -b feature/fooBar`) 39 | 3. Commit your changes (`git commit -am 'Add some fooBar'`) 40 | 4. Push to the branch (`git push origin feature/fooBar`) 41 | 5. Create a new Pull Request 42 | -------------------------------------------------------------------------------- /behaviour/closeable.go: -------------------------------------------------------------------------------- 1 | package behaviour 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/engine" 5 | "github.com/galaco/lambda-client/messages" 6 | "github.com/galaco/tinygametools" 7 | ) 8 | 9 | // Closeable Simple struct to control engine shutdown utilising the internal event manager 10 | type Closeable struct { 11 | target *engine.Engine 12 | } 13 | 14 | // CallbackMouseMove function will shutdown the engine 15 | func (closer Closeable) ReceiveMessage(message tinygametools.Event) { 16 | if message.Type() == messages.TypeKeyDown { 17 | if message.(*messages.KeyDown).Key == tinygametools.KeyEscape { 18 | // Will shutdown the engine at the end of the current loop 19 | closer.target.Close() 20 | } 21 | } 22 | } 23 | 24 | func NewCloseable(target *engine.Engine) *Closeable { 25 | return &Closeable{ 26 | target: target, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /behaviour/controllers/camera.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/engine" 5 | "github.com/galaco/lambda-client/input" 6 | "github.com/galaco/lambda-client/input/keyboard" 7 | "github.com/galaco/lambda-client/scene" 8 | ) 9 | 10 | type Camera struct { 11 | engine.Manager 12 | } 13 | 14 | func (controller *Camera) Update(dt float64) { 15 | cam := scene.Get().CurrentCamera() 16 | if cam == nil { 17 | return 18 | } 19 | if input.GetKeyboard().IsKeyDown(keyboard.KeyW) { 20 | cam.Forwards(dt) 21 | } 22 | if input.GetKeyboard().IsKeyDown(keyboard.KeyA) { 23 | cam.Left(dt) 24 | } 25 | if input.GetKeyboard().IsKeyDown(keyboard.KeyS) { 26 | cam.Backwards(dt) 27 | } 28 | if input.GetKeyboard().IsKeyDown(keyboard.KeyD) { 29 | cam.Right(dt) 30 | } 31 | 32 | cam.Rotate(input.GetMouse().GetCoordinates()[0], 0, input.GetMouse().GetCoordinates()[1]) 33 | } 34 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "gameDirectory": ".", 3 | "video": { 4 | "width": 640, 5 | "height": 480 6 | } 7 | } -------------------------------------------------------------------------------- /container.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | keyvalues "github.com/galaco/KeyValues" 5 | "github.com/galaco/lambda-client/internal/config" 6 | "github.com/galaco/tinygametools" 7 | "github.com/galaco/filesystem" 8 | ) 9 | 10 | // Container 11 | type Container struct { 12 | Config config.Config 13 | GameInfo keyvalues.KeyValue 14 | Filesystem *filesystem.FileSystem 15 | Window tinygametools.Window 16 | Keyboard tinygametools.Keyboard 17 | Mouse tinygametools.Mouse 18 | } 19 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Engine Game engine 8 | // Only 1 should be initialised 9 | type Engine struct { 10 | Managers []IManager 11 | running bool 12 | simulationSpeed float64 13 | } 14 | 15 | // Initialise the engine, and attached managers 16 | func (engine *Engine) Initialise() { 17 | } 18 | 19 | // Run the engine 20 | func (engine *Engine) Run() { 21 | engine.running = true 22 | 23 | // Begin the event manager thread in the background 24 | //event.Manager().ProcessQueue() 25 | // Anything that runs concurrently can start now 26 | for _, manager := range engine.Managers { 27 | manager.RunConcurrent() 28 | } 29 | 30 | dt := 0.0 31 | startingTime := time.Now().UTC() 32 | 33 | for engine.running { 34 | for _, manager := range engine.Managers { 35 | manager.Update(dt) 36 | } 37 | 38 | for _, manager := range engine.Managers { 39 | manager.PostUpdate() 40 | } 41 | 42 | dt = (float64(time.Now().UTC().Sub(startingTime).Nanoseconds()/1000000) / 1000) * engine.simulationSpeed 43 | startingTime = time.Now().UTC() 44 | } 45 | 46 | for _, manager := range engine.Managers { 47 | manager.Unregister() 48 | } 49 | } 50 | 51 | // AddManager Adds a new manager to the engine 52 | func (engine *Engine) AddManager(manager IManager) { 53 | engine.Managers = append(engine.Managers, manager) 54 | manager.Register() 55 | } 56 | 57 | // Close marks the engine to exit at the end of the current loop 58 | func (engine *Engine) Close() { 59 | engine.running = false 60 | } 61 | 62 | // SetSimulationSpeed allows for speeding up and slowing down the game clock 63 | func (engine *Engine) SetSimulationSpeed(multiplier float64) { 64 | if multiplier < 0 { 65 | return 66 | } 67 | engine.simulationSpeed = multiplier 68 | } 69 | 70 | // NewEngine returns a new engine instance 71 | func NewEngine() *Engine { 72 | return &Engine{ 73 | simulationSpeed: 1.0, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /engine/imanager.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | // IManager Generic game manager. 4 | // Different systems should implement these methods 5 | type IManager interface { 6 | Register() 7 | RunConcurrent() 8 | Update(float64) 9 | Unregister() 10 | PostUpdate() 11 | } 12 | -------------------------------------------------------------------------------- /engine/manager.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | // Manager Managers exist to create and handle behaviour. 4 | type Manager struct { 5 | } 6 | 7 | // Register this manager in the engine. This is called by the engine 8 | // when the system is attached. 9 | func (manager *Manager) Register() { 10 | } 11 | 12 | // RunConcurrent If this manager is supported to run concurrently, custom concurrency 13 | // function should be defined here 14 | func (manager *Manager) RunConcurrent() { 15 | } 16 | 17 | // Update Called every update loop. 18 | // dt is the time elapsed since last called 19 | func (manager *Manager) Update(dt float64) { 20 | } 21 | 22 | // Unregister Called when this manager is detached and destroyed by the 23 | // engine 24 | func (manager *Manager) Unregister() { 25 | } 26 | 27 | // PostUpdate Called at the end of each loop. 28 | func (manager *Manager) PostUpdate() { 29 | } 30 | -------------------------------------------------------------------------------- /event/manager.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "github.com/galaco/tinygametools" 4 | 5 | type Name tinygametools.EventName 6 | 7 | var manager *tinygametools.EventManager 8 | 9 | // Dispatcher 10 | func Dispatcher() *tinygametools.EventManager { 11 | if manager == nil { 12 | manager = tinygametools.NewEventManager() 13 | } 14 | return manager 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/galaco/lambda-client 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/galaco/KeyValues v1.4.1 7 | github.com/galaco/bsp v0.2.2 8 | github.com/galaco/gosigl v0.1.1 9 | github.com/galaco/lambda-core v1.1.3 10 | github.com/galaco/source-tools-common v0.1.0 11 | github.com/galaco/tinygametools v0.1.0 12 | github.com/galaco/vtf v1.2.0 // indirect 13 | github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 14 | github.com/go-gl/mathgl v0.0.0-20190713194549-592312d8590a 15 | github.com/galaco/filesystem v0.1.3 16 | github.com/galaco/vmt v0.1.3 // indirect 17 | github.com/gotk3/gotk3 v0.0.0-20191027191019-60cba67d4ea4 // indirect 18 | github.com/inkyblackness/imgui-go v1.10.0 19 | github.com/logrusorgru/aurora v0.0.0-20191017060258-dc85c304c434 // indirect 20 | github.com/sqweek/dialog v0.0.0-20190728103509-6254ed5b0d3c 21 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= 2 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= 3 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 4 | github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= 5 | github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/galaco/KeyValues v1.4.1/go.mod h1:00r0hZpLlOBIHehyWAgUngjKPoo3vCVP25BgWLwOP7E= 8 | github.com/galaco/bsp v0.2.2/go.mod h1:2T3tF0vzvY0NBPrLGe0B5EEQrbG2F0Ur+HWnaSy9YA4= 9 | github.com/galaco/filesystem v0.1.3/go.mod h1:kqyyMRAICNZhUKgPfVQlMNZ3AGsoIY4+rjqly8lUYG8= 10 | github.com/galaco/gosigl v0.1.1/go.mod h1:A4DaiRUW0zzrKYUk94+Ks2aXCGcgISHYM7gO1dtzPr0= 11 | github.com/galaco/lambda-core v1.1.3/go.mod h1:1FttA43jB28ppIboO9ohpT1+UFNMmQSnoXAw9ig8SXA= 12 | github.com/galaco/loggy v0.0.0-20190629005848-af043014a903/go.mod h1:MCpME2n8JUMLql/kF69GPXQKkJciiNTBI/otWCkTjw8= 13 | github.com/galaco/packrect v0.0.0-20190221101750-d10c60f871c0/go.mod h1:1QOQWDBGRc6sT0tglK344YodNuAGsj6WPqZXzzpv6f4= 14 | github.com/galaco/source-tools-common v0.1.0/go.mod h1:+TApRVomBN759va2qrv5GaLuCaK2AWxR0tn9oc2U73Q= 15 | github.com/galaco/studiomodel v0.1.2/go.mod h1:8bzTjcsmb1OsLLaf2ES60avF9WfAaZtVGKUw/IXe14c= 16 | github.com/galaco/tinygametools v0.1.0/go.mod h1:uhxliQyadXV+GSlkwsQMUPv45lr61vNv1Z8N+CsDvFQ= 17 | github.com/galaco/vmf v0.0.0-20180309175744-7a9ccb6e8abb/go.mod h1:+hpnZQBHJ5xrERgGMr6f/KO0ZkxEkrvjqr3RI9aNHes= 18 | github.com/galaco/vmf v1.0.0/go.mod h1:+hpnZQBHJ5xrERgGMr6f/KO0ZkxEkrvjqr3RI9aNHes= 19 | github.com/galaco/vmt v0.1.3/go.mod h1:Kf6Dq8HbWXVSKaC0bWR07XNs+Zw2hcKwLzyG3ChXN7E= 20 | github.com/galaco/vpk2 v0.0.0-20181012095330-21e4d1f6c888/go.mod h1:jL22XAWuUlYUmONuamxDdbDlGJhuOFkqNRPJwuBA3X8= 21 | github.com/galaco/vtf v1.1.1/go.mod h1:/qXLzN9Et3Ed61RUR4rYXO/eMMFgvEyc4opw/Nw6xkM= 22 | github.com/galaco/vtf v1.2.0/go.mod h1:VV7M0uI2bOQkTScuYy/pZcH6IqXWZdH2DLx7HAAwAl8= 23 | github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= 24 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 25 | github.com/go-gl/mathgl v0.0.0-20180804195959-cdf14b6b8f8a/go.mod h1:dvrdneKbyWbK2skTda0nM4B9zSlS2GZSnjX7itr/skQ= 26 | github.com/go-gl/mathgl v0.0.0-20190713194549-592312d8590a/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= 27 | github.com/golang-source-engine/filesystem v0.1.2/go.mod h1:HKc/2NsiY0wj9uRSM6s5AQIt/8J9T7qx2nITY8JBKEU= 28 | github.com/golang-source-engine/stringtable v0.1.0/go.mod h1:1wsLmMF67JvlpOSe0JVmTy43io7OfvcD3AMEifKIrIA= 29 | github.com/golang-source-engine/vmt v0.1.1/go.mod h1:devs1P7sA/Spthvxw8+wzuEsN3Ibadbia6n3SQQfdW8= 30 | github.com/gotk3/gotk3 v0.0.0-20191027191019-60cba67d4ea4/go.mod h1:Eew3QBwAOBTrfFFDmsDE5wZWbcagBL1NUslj1GhRveo= 31 | github.com/inkyblackness/imgui-go v1.10.0/go.mod h1:Mki1fp0SBQ0dq2wkAPKjI3Zod0fgpEFsPM9cGUoCNi8= 32 | github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 33 | github.com/logrusorgru/aurora v0.0.0-20191017060258-dc85c304c434/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 34 | github.com/mattn/go-gtk v0.0.0-20180216084204-5a311a1830ab/go.mod h1:PwzwfeB5syFHXORC3MtPylVcjIoTDT/9cvkKpEndGVI= 35 | github.com/mattn/go-pointer v0.0.0-20171114154726-1d30dc4b6f28/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 36 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/skelterjohn/go.wde v0.0.0-20180104102407-a0324cbf3ffe/go.mod h1:zXxNsJHeUYIqpg890APBNEn9GoCbA4Cdnvuv3mx4fBk= 39 | github.com/sqweek/dialog v0.0.0-20190728103509-6254ed5b0d3c/go.mod h1:QSrNdZLZB8VoFPGlZ2vDuA2oNaVdhld3g0PZLc7soX8= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 42 | golang.org/x/image v0.0.0-20190209060608-ef4a1470e0dc/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 43 | golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 44 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 45 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | -------------------------------------------------------------------------------- /input/keyboard.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/input/keyboard" 5 | "github.com/galaco/lambda-client/messages" 6 | "github.com/galaco/lambda-core/event" 7 | "github.com/galaco/tinygametools" 8 | ) 9 | 10 | // Keyboard key wrapper 11 | type Keyboard struct { 12 | keysDown [1024]bool 13 | } 14 | 15 | // IsKeyDown Check if a specific key is pressed 16 | func (keyboard *Keyboard) IsKeyDown(key keyboard.Key) bool { 17 | return keyboard.keysDown[int(key)] 18 | } 19 | 20 | // CallbackMouseMove Event manager message receiver. 21 | // Used to catch key events from the window library 22 | func (keyboard *Keyboard) ReceiveMessage(message tinygametools.Event) { 23 | switch message.Type() { 24 | case messages.TypeKeyDown: 25 | keyboard.keysDown[int(message.(*messages.KeyDown).Key)] = true 26 | case messages.TypeKeyReleased: 27 | keyboard.keysDown[int(message.(*messages.KeyReleased).Key)] = false 28 | } 29 | } 30 | 31 | func (keyboard *Keyboard) SendMessage() event.IMessage { 32 | return nil 33 | } 34 | 35 | var staticKeyboard Keyboard 36 | 37 | // GetKeyboard return static keyboard 38 | func GetKeyboard() *Keyboard { 39 | return &staticKeyboard 40 | } 41 | -------------------------------------------------------------------------------- /input/keyboard/keys.go: -------------------------------------------------------------------------------- 1 | package keyboard 2 | 3 | import "github.com/go-gl/glfw/v3.2/glfw" 4 | 5 | type Key int 6 | 7 | const ( 8 | KeyUnknown Key = Key(glfw.KeyUnknown) 9 | KeySpace Key = Key(glfw.KeySpace) 10 | KeyApostrophe Key = Key(glfw.KeyApostrophe) 11 | KeyComma Key = Key(glfw.KeyComma) 12 | KeyMinus Key = Key(glfw.KeyMinus) 13 | KeyPeriod Key = Key(glfw.KeyPeriod) 14 | KeySlash Key = Key(glfw.KeySlash) 15 | Key0 Key = Key(glfw.Key0) 16 | Key1 Key = Key(glfw.Key1) 17 | Key2 Key = Key(glfw.Key2) 18 | Key3 Key = Key(glfw.Key3) 19 | Key4 Key = Key(glfw.Key4) 20 | Key5 Key = Key(glfw.Key5) 21 | Key6 Key = Key(glfw.Key6) 22 | Key7 Key = Key(glfw.Key7) 23 | Key8 Key = Key(glfw.Key8) 24 | Key9 Key = Key(glfw.Key9) 25 | KeySemicolon Key = Key(glfw.KeySemicolon) 26 | KeyEqual Key = Key(glfw.KeyEqual) 27 | KeyA Key = Key(glfw.KeyA) 28 | KeyB Key = Key(glfw.KeyB) 29 | KeyC Key = Key(glfw.KeyC) 30 | KeyD Key = Key(glfw.KeyD) 31 | KeyE Key = Key(glfw.KeyE) 32 | KeyF Key = Key(glfw.KeyF) 33 | KeyG Key = Key(glfw.KeyG) 34 | KeyH Key = Key(glfw.KeyH) 35 | KeyI Key = Key(glfw.KeyI) 36 | KeyJ Key = Key(glfw.KeyJ) 37 | KeyK Key = Key(glfw.KeyK) 38 | KeyL Key = Key(glfw.KeyL) 39 | KeyM Key = Key(glfw.KeyM) 40 | KeyN Key = Key(glfw.KeyN) 41 | KeyO Key = Key(glfw.KeyO) 42 | KeyP Key = Key(glfw.KeyP) 43 | KeyQ Key = Key(glfw.KeyQ) 44 | KeyR Key = Key(glfw.KeyR) 45 | KeyS Key = Key(glfw.KeyS) 46 | KeyT Key = Key(glfw.KeyT) 47 | KeyU Key = Key(glfw.KeyU) 48 | KeyV Key = Key(glfw.KeyV) 49 | KeyW Key = Key(glfw.KeyW) 50 | KeyX Key = Key(glfw.KeyX) 51 | KeyY Key = Key(glfw.KeyY) 52 | KeyZ Key = Key(glfw.KeyZ) 53 | KeyLeftBracket Key = Key(glfw.KeyLeftBracket) 54 | KeyBackslash Key = Key(glfw.KeyBackslash) 55 | KeyRightBracket Key = Key(glfw.KeyRightBracket) 56 | KeyGraveAccent Key = Key(glfw.KeyGraveAccent) 57 | KeyWorld1 Key = Key(glfw.KeyWorld1) 58 | KeyWorld2 Key = Key(glfw.KeyWorld2) 59 | KeyEscape Key = Key(glfw.KeyEscape) 60 | KeyEnter Key = Key(glfw.KeyEnter) 61 | KeyTab Key = Key(glfw.KeyTab) 62 | KeyBackspace Key = Key(glfw.KeyBackspace) 63 | KeyInsert Key = Key(glfw.KeyInsert) 64 | KeyDelete Key = Key(glfw.KeyDelete) 65 | KeyRight Key = Key(glfw.KeyRight) 66 | KeyLeft Key = Key(glfw.KeyLeft) 67 | KeyDown Key = Key(glfw.KeyDown) 68 | KeyUp Key = Key(glfw.KeyUp) 69 | KeyPageUp Key = Key(glfw.KeyPageUp) 70 | KeyPageDown Key = Key(glfw.KeyPageDown) 71 | KeyHome Key = Key(glfw.KeyHome) 72 | KeyEnd Key = Key(glfw.KeyEnd) 73 | KeyCapsLock Key = Key(glfw.KeyCapsLock) 74 | KeyScrollLock Key = Key(glfw.KeyScrollLock) 75 | KeyNumLock Key = Key(glfw.KeyNumLock) 76 | KeyPrintScreen Key = Key(glfw.KeyPrintScreen) 77 | KeyPause Key = Key(glfw.KeyPause) 78 | KeyF1 Key = Key(glfw.KeyF1) 79 | KeyF2 Key = Key(glfw.KeyF2) 80 | KeyF3 Key = Key(glfw.KeyF3) 81 | KeyF4 Key = Key(glfw.KeyF4) 82 | KeyF5 Key = Key(glfw.KeyF5) 83 | KeyF6 Key = Key(glfw.KeyF6) 84 | KeyF7 Key = Key(glfw.KeyF7) 85 | KeyF8 Key = Key(glfw.KeyF8) 86 | KeyF9 Key = Key(glfw.KeyF9) 87 | KeyF10 Key = Key(glfw.KeyF10) 88 | KeyF11 Key = Key(glfw.KeyF11) 89 | KeyF12 Key = Key(glfw.KeyF12) 90 | KeyF13 Key = Key(glfw.KeyF13) 91 | KeyF14 Key = Key(glfw.KeyF14) 92 | KeyF15 Key = Key(glfw.KeyF15) 93 | KeyF16 Key = Key(glfw.KeyF16) 94 | KeyF17 Key = Key(glfw.KeyF17) 95 | KeyF18 Key = Key(glfw.KeyF18) 96 | KeyF19 Key = Key(glfw.KeyF19) 97 | KeyF20 Key = Key(glfw.KeyF20) 98 | KeyF21 Key = Key(glfw.KeyF21) 99 | KeyF22 Key = Key(glfw.KeyF22) 100 | KeyF23 Key = Key(glfw.KeyF23) 101 | KeyF24 Key = Key(glfw.KeyF24) 102 | KeyF25 Key = Key(glfw.KeyF25) 103 | KeyKP0 Key = Key(glfw.KeyKP0) 104 | KeyKP1 Key = Key(glfw.KeyKP1) 105 | KeyKP2 Key = Key(glfw.KeyKP2) 106 | KeyKP3 Key = Key(glfw.KeyKP3) 107 | KeyKP4 Key = Key(glfw.KeyKP4) 108 | KeyKP5 Key = Key(glfw.KeyKP5) 109 | KeyKP6 Key = Key(glfw.KeyKP6) 110 | KeyKP7 Key = Key(glfw.KeyKP7) 111 | KeyKP8 Key = Key(glfw.KeyKP8) 112 | KeyKP9 Key = Key(glfw.KeyKP9) 113 | KeyKPDecimal Key = Key(glfw.KeyKPDecimal) 114 | KeyKPDivide Key = Key(glfw.KeyKPDivide) 115 | KeyKPMultiply Key = Key(glfw.KeyKPMultiply) 116 | KeyKPSubtract Key = Key(glfw.KeyKPSubtract) 117 | KeyKPAdd Key = Key(glfw.KeyKPAdd) 118 | KeyKPEnter Key = Key(glfw.KeyKPEnter) 119 | KeyKPEqual Key = Key(glfw.KeyKPEqual) 120 | KeyLeftShift Key = Key(glfw.KeyLeftShift) 121 | KeyLeftControl Key = Key(glfw.KeyLeftControl) 122 | KeyLeftAlt Key = Key(glfw.KeyLeftAlt) 123 | KeyLeftSuper Key = Key(glfw.KeyLeftSuper) 124 | KeyRightShift Key = Key(glfw.KeyRightShift) 125 | KeyRightControl Key = Key(glfw.KeyRightControl) 126 | KeyRightAlt Key = Key(glfw.KeyRightAlt) 127 | KeyRightSuper Key = Key(glfw.KeyRightSuper) 128 | KeyMenu Key = Key(glfw.KeyMenu) 129 | KeyLast Key = Key(glfw.KeyLast) 130 | ) 131 | -------------------------------------------------------------------------------- /input/manager.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/engine" 5 | "github.com/galaco/lambda-client/event" 6 | "github.com/galaco/lambda-client/input/keyboard" 7 | "github.com/galaco/lambda-client/messages" 8 | "github.com/galaco/tinygametools" 9 | "github.com/go-gl/glfw/v3.2/glfw" 10 | "github.com/go-gl/mathgl/mgl64" 11 | ) 12 | 13 | // manager handles user input from mouse and keyboard 14 | // in a specific window 15 | type Manager struct { 16 | engine.Manager 17 | MouseCoordinates mgl64.Vec2 18 | window *tinygametools.Window 19 | lockMouse bool 20 | 21 | mouse *tinygametools.Mouse 22 | keyboard *tinygametools.Keyboard 23 | } 24 | 25 | // Register prepares this manager to listen for events from a window 26 | func (manager *Manager) Register() { 27 | manager.keyboard.AddKeyCallback(manager.KeyCallback) 28 | manager.mouse.AddMousePosCallback(manager.MouseCallback) 29 | 30 | manager.keyboard.RegisterCallbacks(manager.window) 31 | manager.mouse.RegisterCallbacks(manager.window) 32 | 33 | _ = event.Dispatcher().Subscribe(messages.TypeKeyDown, GetKeyboard().ReceiveMessage, GetKeyboard()) 34 | _ = event.Dispatcher().Subscribe(messages.TypeKeyReleased, GetKeyboard().ReceiveMessage, GetKeyboard()) 35 | _ = event.Dispatcher().Subscribe(messages.TypeMouseMove, GetMouse().CallbackMouseMove, GetMouse()) 36 | } 37 | 38 | // Update prepares data constructs that represent mouse & keyboard state with 39 | // updated information on the current input state. 40 | func (manager *Manager) Update(dt float64) { 41 | // Get window size 42 | x, y := manager.window.Handle().GetSize() 43 | if GetKeyboard().IsKeyDown(keyboard.KeyE) { 44 | manager.lockMouse = true 45 | manager.window.Handle().SetCursorPos(float64(x)/2, float64(y)/2) 46 | manager.window.Handle().SetInputMode(glfw.CursorMode, glfw.CursorDisabled) 47 | } else { 48 | manager.lockMouse = false 49 | manager.window.Handle().SetInputMode(glfw.CursorMode, glfw.CursorNormal) 50 | } 51 | 52 | GetMouse().Update() 53 | glfw.PollEvents() 54 | } 55 | func (manager *Manager) PostUpdate() { 56 | GetMouse().PostUpdate() 57 | } 58 | 59 | // Unregister 60 | func (manager *Manager) Unregister() { 61 | 62 | } 63 | 64 | // KeyCallback called whenever a key event occurs 65 | func (manager *Manager) KeyCallback(window *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { 66 | switch action { 67 | case glfw.Press: 68 | _ = event.Dispatcher().Publish(&messages.KeyDown{Key: tinygametools.Key(key)}) 69 | case glfw.Repeat: 70 | _ = event.Dispatcher().Publish(&messages.KeyHeld{Key: tinygametools.Key(key)}) 71 | case glfw.Release: 72 | _ = event.Dispatcher().Publish(&messages.KeyReleased{Key: tinygametools.Key(key)}) 73 | } 74 | } 75 | 76 | // MouseCallback called whenever a mouse event occurs 77 | func (manager *Manager) MouseCallback(window *glfw.Window, xpos float64, ypos float64) { 78 | if manager.lockMouse { 79 | manager.MouseCoordinates[0], manager.MouseCoordinates[1] = window.GetCursorPos() 80 | w, h := window.GetSize() 81 | _ = event.Dispatcher().Publish(&messages.MouseMove{ 82 | XY: [2]float64{ 83 | float64(float64(w/2) - manager.MouseCoordinates[0]), 84 | float64(float64(h/2) - manager.MouseCoordinates[1]), 85 | }, 86 | }) 87 | window.SetCursorPos(float64(w/2), float64(h/2)) 88 | } 89 | } 90 | 91 | // NewInputManager 92 | func NewInputManager(win *tinygametools.Window, mouse *tinygametools.Mouse, keyboard *tinygametools.Keyboard) *Manager { 93 | manager := &Manager{ 94 | window: win, 95 | mouse: mouse, 96 | keyboard: keyboard, 97 | } 98 | 99 | return manager 100 | } 101 | -------------------------------------------------------------------------------- /input/mouse.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/messages" 5 | "github.com/galaco/lambda-core/event" 6 | "github.com/galaco/tinygametools" 7 | "github.com/go-gl/mathgl/mgl32" 8 | ) 9 | 10 | // Mouse information, about change from previous poll. 11 | // Note: Mouse is a struct containing mouse information, it doesn't have 12 | // any direct interaction with the window 13 | type Mouse struct { 14 | change mgl32.Vec2 15 | } 16 | 17 | // GetCoordinates return current mouse position 18 | func (mouse *Mouse) GetCoordinates() mgl32.Vec2 { 19 | return mouse.change 20 | } 21 | 22 | // CallbackMouseMove mouse receives updated info from the event queue about 23 | // mouse interaction 24 | func (mouse *Mouse) CallbackMouseMove(message tinygametools.Event) { 25 | msg := message.(*messages.MouseMove) 26 | mouse.change[0] = float32(msg.XY[0]) 27 | mouse.change[1] = float32(msg.XY[1]) 28 | } 29 | 30 | // Update The Mouse should be reset to screen center 31 | func (mouse *Mouse) Update() { 32 | } 33 | 34 | func (mouse *Mouse) PostUpdate() { 35 | mouse.change[0] = 0 36 | mouse.change[1] = 0 37 | } 38 | 39 | func (mouse *Mouse) SendMessage() event.IMessage { 40 | return nil 41 | } 42 | 43 | var mouse Mouse 44 | 45 | // GetMouse return static mouse 46 | func GetMouse() *Mouse { 47 | return &mouse 48 | } 49 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | ) 7 | 8 | const minWidth = 320 9 | const minHeight = 240 10 | 11 | // Project configuration properties 12 | // Engine needs to know where to locate its game data 13 | type Config struct { 14 | GameDirectory string 15 | Video struct { 16 | Width int 17 | Height int 18 | } 19 | } 20 | 21 | // @TODO Implement something nicer than this scoped variable 22 | var config Config 23 | 24 | // Get returns (kind-of) static config object 25 | func Get() *Config { 26 | return &config 27 | } 28 | 29 | // Load attempts to open and unmarshall 30 | // json configuration 31 | func Load(path string) (*Config, error) { 32 | data, err := ioutil.ReadFile(path) 33 | if err != nil { 34 | return &config, err 35 | } 36 | 37 | err = json.Unmarshal(data, &config) 38 | if err != nil { 39 | return &config, err 40 | } 41 | 42 | validate() 43 | 44 | return &config, nil 45 | } 46 | 47 | // validate that expected parameters with known 48 | // boundaries or limitation fall within expectations. 49 | func validate() { 50 | if config.Video.Width < minWidth { 51 | config.Video.Width = minWidth 52 | } 53 | 54 | if config.Video.Height < minHeight { 55 | config.Video.Height = minHeight 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGet(t *testing.T) { 8 | c := Get() 9 | 10 | if c == nil { 11 | t.Error("exepcted Config, but got nil") 12 | } 13 | } 14 | 15 | func TestLoad(t *testing.T) { 16 | _, err := Load("./../../config.example.json") 17 | 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/debug/log.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import "os" 4 | 5 | type stdOut struct{} 6 | 7 | func (log *stdOut) Write(data []byte) (n int, err error) { 8 | line := string(data) + "\n" 9 | return os.Stdout.Write([]byte(line)) 10 | } 11 | 12 | func NewStdOut() *stdOut { 13 | return &stdOut{} 14 | } 15 | -------------------------------------------------------------------------------- /internal/debug/profiler.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "os" 5 | "runtime/pprof" 6 | ) 7 | 8 | func StartProfiling(filename string) error { 9 | f, err := os.Create(filename) 10 | if err != nil { 11 | return err 12 | } 13 | return pprof.StartCPUProfile(f) 14 | } 15 | 16 | func StopProfiling() { 17 | pprof.StopCPUProfile() 18 | } 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/galaco/KeyValues" 6 | "github.com/galaco/lambda-client/behaviour" 7 | "github.com/galaco/lambda-client/behaviour/controllers" 8 | "github.com/galaco/lambda-client/engine" 9 | "github.com/galaco/lambda-client/event" 10 | "github.com/galaco/lambda-client/input" 11 | "github.com/galaco/lambda-client/internal/config" 12 | "github.com/galaco/lambda-client/internal/debug" 13 | "github.com/galaco/lambda-client/messages" 14 | "github.com/galaco/lambda-client/renderer" 15 | "github.com/galaco/lambda-client/scene" 16 | "github.com/galaco/lambda-client/ui/dialogs" 17 | "github.com/galaco/lambda-client/window" 18 | "github.com/galaco/lambda-core/game" 19 | "github.com/galaco/lambda-core/lib/gameinfo" 20 | "github.com/galaco/lambda-core/lib/util" 21 | "github.com/galaco/lambda-core/resource" 22 | "github.com/galaco/tinygametools" 23 | "github.com/galaco/filesystem" 24 | "log" 25 | "runtime" 26 | ) 27 | 28 | func main() { 29 | runtime.LockOSThread() 30 | 31 | defer func() { 32 | if recovered := recover(); recovered != nil { 33 | fmt.Println("Caught panic:", recovered) 34 | dialogs.ErrorMessage(fmt.Errorf("%s", recovered)) 35 | } 36 | }() 37 | 38 | util.Logger().SetWriter(debug.NewStdOut()) 39 | util.Logger().EnablePretty() 40 | 41 | container := Container{} 42 | 43 | // Load GameInfo.txt 44 | // GameInfo.txt includes fundamental properties about the game 45 | // and its resources locations 46 | cfg, err := config.Load("./config.json") 47 | if err != nil { 48 | util.Logger().Panic(err) 49 | } 50 | container.Config = *cfg 51 | // Load GameInfo 52 | gameInfo, err := gameinfo.LoadConfig(cfg.GameDirectory) 53 | if err != nil { 54 | util.Logger().Panic(err) 55 | } 56 | container.GameInfo = *gameInfo 57 | 58 | // Register GameInfo.txt referenced resource paths 59 | // Filesystem module needs to know about all the possible resource 60 | // locations it can search. 61 | container.Filesystem, err = filesystem.CreateFilesystemFromGameInfoDefinitions(cfg.GameDirectory, gameInfo, true) 62 | if err != nil { 63 | util.Logger().Error(err) 64 | } else { 65 | for _, loc := range container.Filesystem.EnumerateResourcePaths() { 66 | util.Logger().Notice(fmt.Sprintf("Loaded resource path: %s", loc)) 67 | } 68 | } 69 | 70 | // Explicitly define fallbacks for missing resources 71 | // Defaults are defined, but if HL2 assets are not readable, then 72 | // the default may not be readable 73 | resource.Manager().SetErrorModelName("models/error.mdl") 74 | resource.Manager().SetErrorTextureName("materials/error.vtf") 75 | defer resource.Manager().Empty() 76 | 77 | // General engine setup 78 | Application := engine.NewEngine() 79 | Application.Initialise() 80 | 81 | // Game specific setup 82 | gameName := SetGame(&game.CounterstrikeSource{}, gameInfo) 83 | 84 | // Create window 85 | win, err := tinygametools.NewWindow(config.Get().Video.Width, config.Get().Video.Height, gameName) 86 | if err != nil { 87 | util.Logger().Panic(err) 88 | } 89 | win.Handle().MakeContextCurrent() 90 | container.Window = *win 91 | 92 | container.Mouse = *tinygametools.NewMouse() 93 | container.Keyboard = *tinygametools.NewKeyboard() 94 | 95 | Application.AddManager(window.NewWindowManager(&container.Window)) 96 | Application.AddManager(input.NewInputManager(&container.Window, &container.Mouse, &container.Keyboard)) 97 | //vgui := ui.NewGUIManager(&container.Window) 98 | renderManager := renderer.NewRenderManager(&container.Window) 99 | //renderManager.SetUIMasterPanel(vgui.MasterPanel()) 100 | Application.AddManager(renderManager) 101 | Application.AddManager(&controllers.Camera{}) 102 | 103 | //_ = vgui.LoadVGUIResource(container.Filesystem, "gamemenu") 104 | //vgui.MasterPanel().Resize(float64(cfg.Video.Width), float64(cfg.Video.Width)) 105 | //Application.AddManager(vgui) 106 | 107 | RegisterShutdownMethod(Application) 108 | 109 | sceneName, loadSceneError := dialogs.OpenFile("Valve BSP files", "bsp") 110 | if loadSceneError != nil { 111 | dialogs.ErrorMessage(loadSceneError) 112 | log.Fatal() 113 | } else { 114 | scene.LoadFromFile(sceneName, container.Filesystem) 115 | } 116 | 117 | // Start 118 | Application.SetSimulationSpeed(10) 119 | Application.Run() 120 | } 121 | 122 | // SetGame registers game entities and returns game name 123 | func SetGame(proj game.IGame, gameInfo *keyvalues.KeyValue) string { 124 | windowName := "lambda-client: A BSP Viewer" 125 | gameNode, _ := gameInfo.Find("game") 126 | if gameNode != nil { 127 | windowName, _ = gameNode.AsString() 128 | } 129 | 130 | proj.RegisterEntityClasses() 131 | 132 | return windowName 133 | } 134 | 135 | // RegisterShutdownMethod Implements a way of shutting down the engine 136 | func RegisterShutdownMethod(app *engine.Engine) { 137 | _ = event.Dispatcher().Subscribe(messages.TypeKeyDown, behaviour.NewCloseable(app).ReceiveMessage, app) 138 | } 139 | -------------------------------------------------------------------------------- /messages/keydown.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | import ( 4 | "github.com/galaco/tinygametools" 5 | ) 6 | 7 | const TypeKeyDown = tinygametools.EventName("KeyDown") 8 | 9 | // KeyDown event object for keydown 10 | type KeyDown struct { 11 | Key tinygametools.Key 12 | } 13 | 14 | // Type returns message type 15 | func (message *KeyDown) Type() tinygametools.EventName { 16 | return TypeKeyDown 17 | } 18 | 19 | // Message 20 | func (message *KeyDown) Message() interface{} { 21 | return message.Key 22 | } 23 | -------------------------------------------------------------------------------- /messages/keyheld.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | import ( 4 | "github.com/galaco/tinygametools" 5 | ) 6 | 7 | const TypeKeyHeld = tinygametools.EventName("KeyHeld") 8 | 9 | // KeyHeld event object for when key is held down 10 | type KeyHeld struct { 11 | Key tinygametools.Key 12 | } 13 | 14 | func (message *KeyHeld) Type() tinygametools.EventName { 15 | return TypeKeyHeld 16 | } 17 | 18 | func (message *KeyHeld) Message() interface{} { 19 | return message.Key 20 | } 21 | -------------------------------------------------------------------------------- /messages/keyreleased.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | import ( 4 | "github.com/galaco/tinygametools" 5 | ) 6 | 7 | const TypeKeyReleased = tinygametools.EventName("KeyReleased") 8 | 9 | // KeyReleased event object for key released 10 | type KeyReleased struct { 11 | Key tinygametools.Key 12 | } 13 | 14 | func (message *KeyReleased) Type() tinygametools.EventName { 15 | return TypeKeyReleased 16 | } 17 | 18 | func (message *KeyReleased) Message() interface{} { 19 | return message.Key 20 | } 21 | -------------------------------------------------------------------------------- /messages/mousemove.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | import ( 4 | "github.com/galaco/tinygametools" 5 | ) 6 | 7 | const TypeMouseMove = tinygametools.EventName("MouseMove") 8 | 9 | // MouseMove event object for when mouse is moved 10 | type MouseMove struct { 11 | XY [2]float64 12 | } 13 | 14 | func (message *MouseMove) Type() tinygametools.EventName { 15 | return TypeMouseMove 16 | } 17 | 18 | func (message *MouseMove) Message() interface{} { 19 | return message.XY 20 | } 21 | -------------------------------------------------------------------------------- /renderer/cache/prop.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/galaco/lambda-core/entity" 5 | entity2 "github.com/galaco/lambda-core/game/entity" 6 | "github.com/galaco/lambda-core/model" 7 | ) 8 | 9 | type PropCache struct { 10 | cacheList []Entry 11 | } 12 | 13 | func (c *PropCache) NeedsRecache() bool { 14 | return len(c.cacheList) == 0 15 | } 16 | 17 | func (c *PropCache) Add(props ...entity.IEntity) { 18 | for _, prop := range props { 19 | c.cacheList = append(c.cacheList, Entry{ 20 | Transform: prop.Transform(), 21 | Model: prop.(entity2.IProp).Model(), 22 | }) 23 | } 24 | } 25 | 26 | func (c *PropCache) All() *[]Entry { 27 | return &c.cacheList 28 | } 29 | 30 | type Entry struct { 31 | Transform *entity.Transform 32 | Model *model.Model 33 | } 34 | 35 | func NewPropCache(props ...entity.IEntity) *PropCache { 36 | c := &PropCache{} 37 | 38 | if len(props) > 0 { 39 | c.Add(props...) 40 | } 41 | 42 | return c 43 | } 44 | -------------------------------------------------------------------------------- /renderer/camera/frustum.go: -------------------------------------------------------------------------------- 1 | package camera 2 | 3 | import ( 4 | "github.com/galaco/lambda-core/entity" 5 | "github.com/go-gl/mathgl/mgl32" 6 | "math" 7 | ) 8 | 9 | const ( 10 | planeRight = 0 11 | planeLeft = 1 12 | planeBottom = 2 13 | planeTop = 3 14 | planeBack = 4 15 | planeFront = 5 16 | planeNormalX = 0 17 | planeNormalY = 1 18 | planeNormalZ = 2 19 | planeToOrigin = 3 20 | ) 21 | 22 | // Frustum 23 | // based on https://gist.github.com/jimmikaelkael/2e4ffa5712d61816c7ca 24 | type Frustum struct { 25 | planes [6][4]float32 26 | } 27 | 28 | // IsCuboidInFrustum 29 | func (frustum *Frustum) IsCuboidInFrustum(mins, maxs mgl32.Vec3) bool { 30 | for i := 0; i < 6; i++ { 31 | if frustum.planes[i][planeNormalX]*mins.X()+frustum.planes[i][planeNormalY]*mins.Y()+frustum.planes[i][planeNormalZ]*mins.Z()+frustum.planes[i][planeToOrigin] > 0 { 32 | continue 33 | } 34 | if frustum.planes[i][planeNormalX]*maxs.X()+frustum.planes[i][planeNormalY]*mins.Y()+frustum.planes[i][planeNormalZ]*mins.Z()+frustum.planes[i][planeToOrigin] > 0 { 35 | continue 36 | } 37 | if frustum.planes[i][planeNormalX]*mins.X()+frustum.planes[i][planeNormalY]*maxs.Y()+frustum.planes[i][planeNormalZ]*mins.Z()+frustum.planes[i][planeToOrigin] > 0 { 38 | continue 39 | } 40 | if frustum.planes[i][planeNormalX]*maxs.X()+frustum.planes[i][planeNormalY]*maxs.Y()+frustum.planes[i][planeNormalZ]*mins.Z()+frustum.planes[i][planeToOrigin] > 0 { 41 | continue 42 | } 43 | if frustum.planes[i][planeNormalX]*mins.X()+frustum.planes[i][planeNormalY]*mins.Y()+frustum.planes[i][planeNormalZ]*maxs.Z()+frustum.planes[i][planeToOrigin] > 0 { 44 | continue 45 | } 46 | if frustum.planes[i][planeNormalX]*maxs.X()+frustum.planes[i][planeNormalY]*mins.Y()+frustum.planes[i][planeNormalZ]*maxs.Z()+frustum.planes[i][planeToOrigin] > 0 { 47 | continue 48 | } 49 | if frustum.planes[i][planeNormalX]*mins.X()+frustum.planes[i][planeNormalY]*maxs.Y()+frustum.planes[i][planeNormalZ]*maxs.Z()+frustum.planes[i][planeToOrigin] > 0 { 50 | continue 51 | } 52 | if frustum.planes[i][planeNormalX]*maxs.X()+frustum.planes[i][planeNormalY]*maxs.Y()+frustum.planes[i][planeNormalZ]*maxs.Z()+frustum.planes[i][planeToOrigin] > 0 { 53 | continue 54 | } 55 | 56 | // If we get here, it isn't in the frustum 57 | return false 58 | } 59 | 60 | return true 61 | } 62 | 63 | func (frustum *Frustum) extractPlanes(modelView mgl32.Mat4, proj mgl32.Mat4) { 64 | clip := mgl32.Mat4{} 65 | 66 | clip[0] = modelView[0]*proj[0] + modelView[1]*proj[4] + modelView[2]*proj[8] + modelView[3]*proj[12] 67 | clip[1] = modelView[0]*proj[1] + modelView[1]*proj[5] + modelView[2]*proj[9] + modelView[3]*proj[13] 68 | clip[2] = modelView[0]*proj[2] + modelView[1]*proj[6] + modelView[2]*proj[10] + modelView[3]*proj[14] 69 | clip[3] = modelView[0]*proj[3] + modelView[1]*proj[7] + modelView[2]*proj[11] + modelView[3]*proj[15] 70 | 71 | clip[4] = modelView[4]*proj[0] + modelView[5]*proj[4] + modelView[6]*proj[8] + modelView[7]*proj[12] 72 | clip[5] = modelView[4]*proj[1] + modelView[5]*proj[5] + modelView[6]*proj[9] + modelView[7]*proj[13] 73 | clip[6] = modelView[4]*proj[2] + modelView[5]*proj[6] + modelView[6]*proj[10] + modelView[7]*proj[14] 74 | clip[7] = modelView[4]*proj[3] + modelView[5]*proj[7] + modelView[6]*proj[11] + modelView[7]*proj[15] 75 | 76 | clip[8] = modelView[8]*proj[0] + modelView[9]*proj[4] + modelView[10]*proj[8] + modelView[11]*proj[12] 77 | clip[9] = modelView[8]*proj[1] + modelView[9]*proj[5] + modelView[10]*proj[9] + modelView[11]*proj[13] 78 | clip[10] = modelView[8]*proj[2] + modelView[9]*proj[6] + modelView[10]*proj[10] + modelView[11]*proj[14] 79 | clip[11] = modelView[8]*proj[3] + modelView[9]*proj[7] + modelView[10]*proj[11] + modelView[11]*proj[15] 80 | 81 | clip[12] = modelView[12]*proj[0] + modelView[13]*proj[4] + modelView[14]*proj[8] + modelView[15]*proj[12] 82 | clip[13] = modelView[12]*proj[1] + modelView[13]*proj[5] + modelView[14]*proj[9] + modelView[15]*proj[13] 83 | clip[14] = modelView[12]*proj[2] + modelView[13]*proj[6] + modelView[14]*proj[10] + modelView[15]*proj[14] 84 | clip[15] = modelView[12]*proj[3] + modelView[13]*proj[7] + modelView[14]*proj[11] + modelView[15]*proj[15] 85 | 86 | // Now we actually want to get the sides of the frustum. To do this we take 87 | // the clipping planes we received above and extract the sides from them. 88 | 89 | // This will extract the RIGHT side of the frustum 90 | frustum.planes[planeRight][planeNormalX] = clip[3] - clip[0] 91 | frustum.planes[planeRight][planeNormalY] = clip[7] - clip[4] 92 | frustum.planes[planeRight][planeNormalZ] = clip[11] - clip[8] 93 | frustum.planes[planeRight][planeToOrigin] = clip[15] - clip[12] 94 | 95 | // Now that we have a normal (A,B,C) and a distance (D) to the plane, 96 | // we want to normalize that normal and distance. 97 | 98 | // Normalize the RIGHT side 99 | frustum.normalizePlane(planeRight) 100 | 101 | // This will extract the LEFT side of the frustum 102 | frustum.planes[planeLeft][planeNormalX] = clip[3] + clip[0] 103 | frustum.planes[planeLeft][planeNormalY] = clip[7] + clip[4] 104 | frustum.planes[planeLeft][planeNormalZ] = clip[11] + clip[8] 105 | frustum.planes[planeLeft][planeToOrigin] = clip[15] + clip[12] 106 | 107 | // Normalize the LEFT side 108 | frustum.normalizePlane(planeLeft) 109 | 110 | // This will extract the BOTTOM side of the frustum 111 | frustum.planes[planeBottom][planeNormalX] = clip[3] + clip[1] 112 | frustum.planes[planeBottom][planeNormalY] = clip[7] + clip[5] 113 | frustum.planes[planeBottom][planeNormalZ] = clip[11] + clip[9] 114 | frustum.planes[planeBottom][planeToOrigin] = clip[15] + clip[13] 115 | 116 | // Normalize the BOTTOM side 117 | frustum.normalizePlane(planeBottom) 118 | 119 | // This will extract the TOP side of the frustum 120 | frustum.planes[planeTop][planeNormalX] = clip[3] - clip[1] 121 | frustum.planes[planeTop][planeNormalY] = clip[7] - clip[5] 122 | frustum.planes[planeTop][planeNormalZ] = clip[11] - clip[9] 123 | frustum.planes[planeTop][planeToOrigin] = clip[15] - clip[13] 124 | 125 | // Normalize the TOP side 126 | frustum.normalizePlane(planeTop) 127 | 128 | // This will extract the BACK side of the frustum 129 | frustum.planes[planeBack][planeNormalX] = clip[3] - clip[2] 130 | frustum.planes[planeBack][planeNormalY] = clip[7] - clip[6] 131 | frustum.planes[planeBack][planeNormalZ] = clip[11] - clip[10] 132 | frustum.planes[planeBack][planeToOrigin] = clip[15] - clip[14] 133 | 134 | // Normalize the BACK side 135 | frustum.normalizePlane(planeBack) 136 | 137 | // This will extract the FRONT side of the frustum 138 | frustum.planes[planeFront][planeNormalX] = clip[3] + clip[2] 139 | frustum.planes[planeFront][planeNormalY] = clip[7] + clip[6] 140 | frustum.planes[planeFront][planeNormalZ] = clip[11] + clip[10] 141 | frustum.planes[planeFront][planeToOrigin] = clip[15] + clip[14] 142 | 143 | // Normalize the FRONT side 144 | frustum.normalizePlane(planeFront) 145 | } 146 | 147 | func (frustum *Frustum) normalizePlane(side int) { 148 | // Here we calculate the magnitude of the normal to the plane (point A B C) 149 | // Remember that (A, B, C) is that same thing as the normal's (X, Y, Z). 150 | // To calculate magnitude you use the equation: magnitude = sqrt( x^2 + y^2 + z^2) 151 | magnitude := float32(math.Sqrt( 152 | float64(frustum.planes[side][planeNormalX]*frustum.planes[side][planeNormalX] + 153 | frustum.planes[side][planeNormalY]*frustum.planes[side][planeNormalY] + 154 | frustum.planes[side][planeNormalZ]*frustum.planes[side][planeNormalZ]))) 155 | // Then we divide the plane's values by it's magnitude. 156 | // This makes it easier to work with. 157 | frustum.planes[side][planeNormalX] /= magnitude 158 | frustum.planes[side][planeNormalY] /= magnitude 159 | frustum.planes[side][planeNormalZ] /= magnitude 160 | frustum.planes[side][planeToOrigin] /= magnitude 161 | } 162 | 163 | // FrustumFromCamera 164 | func FrustumFromCamera(camera *entity.Camera) *Frustum { 165 | f := &Frustum{} 166 | f.extractPlanes(camera.ViewMatrix().Mul4(camera.ModelMatrix()), camera.ProjectionMatrix()) 167 | 168 | return f 169 | } 170 | -------------------------------------------------------------------------------- /renderer/gl/bsp/cache.go: -------------------------------------------------------------------------------- 1 | package bsp 2 | 3 | import ( 4 | "github.com/galaco/gosigl" 5 | "github.com/galaco/lambda-core/event" 6 | "github.com/galaco/lambda-core/resource/message" 7 | ) 8 | 9 | var MapGPUResource *gosigl.VertexObject 10 | 11 | func SyncMapToGpu(dispatched event.IMessage) { 12 | msg := dispatched.(*message.MapLoaded) 13 | mesh := msg.Resource.Mesh() 14 | MapGPUResource = gosigl.NewMesh(mesh.Vertices()) 15 | gosigl.CreateVertexAttribute(MapGPUResource, mesh.UVs(), 2) 16 | gosigl.CreateVertexAttribute(MapGPUResource, mesh.Normals(), 3) 17 | if len(mesh.Tangents()) == 0 { 18 | mesh.GenerateTangents() 19 | } 20 | gosigl.CreateVertexAttribute(MapGPUResource, mesh.Tangents(), 4) 21 | gosigl.CreateVertexAttribute(MapGPUResource, mesh.LightmapCoordinates(), 2) 22 | gosigl.FinishMesh() 23 | } 24 | -------------------------------------------------------------------------------- /renderer/gl/material/cache.go: -------------------------------------------------------------------------------- 1 | package material 2 | 3 | import ( 4 | "github.com/galaco/gosigl" 5 | "github.com/galaco/lambda-core/event" 6 | "github.com/galaco/lambda-core/material" 7 | "github.com/galaco/lambda-core/resource/message" 8 | "sync" 9 | ) 10 | 11 | type Cache struct { 12 | textureIdMap map[string]gosigl.TextureBindingId 13 | mut sync.Mutex 14 | } 15 | 16 | func (cache *Cache) FetchCachedTexture(textureName string) gosigl.TextureBindingId { 17 | return cache.textureIdMap[textureName] 18 | } 19 | 20 | func (cache *Cache) SyncTextureToGpu(dispatched event.IMessage) { 21 | msg := dispatched.(*message.MaterialLoaded) 22 | mat := msg.Resource.(*material.Material) 23 | 24 | if mat.Textures.Albedo == nil { 25 | return 26 | } 27 | 28 | cache.mut.Lock() 29 | if _, ok := cache.textureIdMap[mat.Textures.Albedo.FilePath()]; !ok { 30 | cache.textureIdMap[mat.Textures.Albedo.FilePath()] = gosigl.CreateTexture2D( 31 | gosigl.TextureSlot(0), 32 | mat.Textures.Albedo.Width(), 33 | mat.Textures.Albedo.Height(), 34 | mat.Textures.Albedo.PixelDataForFrame(0), 35 | gosigl.PixelFormat(GLTextureFormatFromVtfFormat(mat.Textures.Albedo.Format())), 36 | false) 37 | } 38 | 39 | if mat.Textures.Normal != nil { 40 | if _, ok := cache.textureIdMap[mat.Textures.Normal.FilePath()]; !ok { 41 | cache.textureIdMap[mat.Textures.Normal.FilePath()] = gosigl.CreateTexture2D( 42 | gosigl.TextureSlot(1), 43 | mat.Textures.Normal.Width(), 44 | mat.Textures.Normal.Height(), 45 | mat.Textures.Normal.PixelDataForFrame(0), 46 | gosigl.PixelFormat(GLTextureFormatFromVtfFormat(mat.Textures.Normal.Format())), 47 | false) 48 | } 49 | } 50 | cache.mut.Unlock() 51 | } 52 | 53 | func (cache *Cache) DestroyTextureOnGPU(dispatched event.IMessage) { 54 | msg := dispatched.(*message.MaterialUnloaded) 55 | mat := msg.Resource.(*material.Material) 56 | if mat.Textures.Albedo != nil { 57 | gosigl.DeleteTextures(cache.textureIdMap[mat.Textures.Albedo.FilePath()]) 58 | } 59 | if mat.Textures.Normal != nil { 60 | gosigl.DeleteTextures(cache.textureIdMap[mat.Textures.Normal.FilePath()]) 61 | } 62 | } 63 | 64 | func NewCache() *Cache { 65 | return &Cache{ 66 | textureIdMap: map[string]gosigl.TextureBindingId{}, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /renderer/gl/material/types.go: -------------------------------------------------------------------------------- 1 | package material 2 | 3 | import "github.com/galaco/gosigl" 4 | 5 | // getGLTextureFormat swap vtf format to openGL format 6 | func GLTextureFormatFromVtfFormat(vtfFormat uint32) gosigl.PixelFormat { 7 | switch vtfFormat { 8 | case 0: 9 | return gosigl.RGBA 10 | case 2: 11 | return gosigl.RGB 12 | case 3: 13 | return gosigl.BGR 14 | case 12: 15 | return gosigl.BGRA 16 | case 13: 17 | return gosigl.DXT1 18 | case 14: 19 | return gosigl.DXT3 20 | case 15: 21 | return gosigl.DXT5 22 | default: 23 | return gosigl.RGB 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /renderer/gl/prop/cache.go: -------------------------------------------------------------------------------- 1 | package prop 2 | 3 | import ( 4 | "github.com/galaco/gosigl" 5 | "github.com/galaco/lambda-core/event" 6 | "github.com/galaco/lambda-core/resource/message" 7 | ) 8 | 9 | var ModelIdMap map[string][]*gosigl.VertexObject 10 | 11 | func SyncPropToGpu(dispatched event.IMessage) { 12 | msg := dispatched.(*message.PropLoaded) 13 | if ModelIdMap[msg.Resource.FilePath()] != nil { 14 | return 15 | } 16 | vals := make([]*gosigl.VertexObject, len(msg.Resource.Meshes())) 17 | 18 | for idx, mesh := range msg.Resource.Meshes() { 19 | gpuObject := gosigl.NewMesh(mesh.Vertices()) 20 | gosigl.CreateVertexAttribute(gpuObject, mesh.UVs(), 2) 21 | gosigl.CreateVertexAttribute(gpuObject, mesh.Normals(), 3) 22 | 23 | if len(mesh.Tangents()) == 0 { 24 | mesh.GenerateTangents() 25 | } 26 | gosigl.CreateVertexAttribute(gpuObject, mesh.Tangents(), 4) 27 | gosigl.CreateVertexAttribute(gpuObject, mesh.LightmapCoordinates(), 2) 28 | gosigl.FinishMesh() 29 | vals[idx] = gpuObject 30 | } 31 | ModelIdMap[msg.Resource.FilePath()] = vals 32 | } 33 | 34 | func DestroyPropOnGPU(dispatched event.IMessage) { 35 | msg := dispatched.(*message.PropUnloaded) 36 | for _, i := range ModelIdMap[msg.Resource.FilePath()] { 37 | gosigl.DeleteMesh(i) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /renderer/gl/renderer.go: -------------------------------------------------------------------------------- 1 | package gl 2 | 3 | import ( 4 | "github.com/galaco/gosigl" 5 | "github.com/galaco/lambda-client/renderer/camera" 6 | "github.com/galaco/lambda-client/renderer/gl/bsp" 7 | material2 "github.com/galaco/lambda-client/renderer/gl/material" 8 | "github.com/galaco/lambda-client/renderer/gl/prop" 9 | "github.com/galaco/lambda-client/renderer/gl/shaders" 10 | "github.com/galaco/lambda-client/renderer/gl/shaders/sky" 11 | "github.com/galaco/lambda-client/scene/world" 12 | "github.com/galaco/lambda-core/entity" 13 | "github.com/galaco/lambda-core/event" 14 | "github.com/galaco/lambda-core/lib/util" 15 | "github.com/galaco/lambda-core/material" 16 | "github.com/galaco/lambda-core/mesh" 17 | "github.com/galaco/lambda-core/model" 18 | "github.com/galaco/lambda-core/resource/message" 19 | opengl "github.com/go-gl/gl/v4.1-core/gl" 20 | "github.com/go-gl/mathgl/mgl32" 21 | "math" 22 | ) 23 | 24 | //OpenGL renderer 25 | type Renderer struct { 26 | lightmappedGenericShader gosigl.Context 27 | skyShader gosigl.Context 28 | 29 | currentShaderId uint32 30 | 31 | uniformMap map[uint32]map[string]int32 32 | 33 | materialCache *material2.Cache 34 | 35 | matrices struct { 36 | view mgl32.Mat4 37 | projection mgl32.Mat4 38 | } 39 | 40 | activeCamera *entity.Camera 41 | viewFrustum *camera.Frustum 42 | } 43 | 44 | // LoadShaders Loads shaders and sets necessary constants for opengls state machine 45 | func (renderer *Renderer) LoadShaders() { 46 | renderer.materialCache = material2.NewCache() 47 | prop.ModelIdMap = map[string][]*gosigl.VertexObject{} 48 | 49 | event.Manager().Listen(message.TypeMaterialLoaded, renderer.materialCache.SyncTextureToGpu) 50 | event.Manager().Listen(message.TypeMaterialUnloaded, renderer.materialCache.DestroyTextureOnGPU) 51 | event.Manager().Listen(message.TypeModelLoaded, prop.SyncPropToGpu) 52 | event.Manager().Listen(message.TypeModelUnloaded, prop.DestroyPropOnGPU) 53 | event.Manager().Listen(message.TypeMapLoaded, bsp.SyncMapToGpu) 54 | 55 | renderer.lightmappedGenericShader = gosigl.NewShader() 56 | err := renderer.lightmappedGenericShader.AddShader(shaders.Vertex, gosigl.VertexShader) 57 | if err != nil { 58 | util.Logger().Panic(err) 59 | } 60 | err = renderer.lightmappedGenericShader.AddShader(shaders.Fragment, gosigl.FragmentShader) 61 | if err != nil { 62 | util.Logger().Panic(err) 63 | } 64 | renderer.lightmappedGenericShader.Finalize() 65 | renderer.skyShader = gosigl.NewShader() 66 | err = renderer.skyShader.AddShader(sky.Vertex, opengl.VERTEX_SHADER) 67 | if err != nil { 68 | util.Logger().Panic(err) 69 | } 70 | err = renderer.skyShader.AddShader(sky.Fragment, opengl.FRAGMENT_SHADER) 71 | if err != nil { 72 | util.Logger().Panic(err) 73 | } 74 | renderer.skyShader.Finalize() 75 | 76 | //matrices 77 | skyShaderMap := map[string]int32{} 78 | skyShaderMap["model"] = renderer.skyShader.GetUniform("model") 79 | skyShaderMap["projection"] = renderer.skyShader.GetUniform("projection") 80 | skyShaderMap["view"] = renderer.skyShader.GetUniform("view") 81 | skyShaderMap["cubemapTexture"] = renderer.lightmappedGenericShader.GetUniform("cubemapTexture") 82 | renderer.uniformMap[renderer.skyShader.Id()] = skyShaderMap 83 | 84 | renderer.lightmappedGenericShader.UseProgram() 85 | lightmappedGenericShaderMap := map[string]int32{} 86 | lightmappedGenericShaderMap["model"] = renderer.lightmappedGenericShader.GetUniform("model") 87 | lightmappedGenericShaderMap["projection"] = renderer.lightmappedGenericShader.GetUniform("projection") 88 | lightmappedGenericShaderMap["view"] = renderer.lightmappedGenericShader.GetUniform("view") 89 | //material properties 90 | lightmappedGenericShaderMap["albedoSampler"] = renderer.lightmappedGenericShader.GetUniform("albedoSampler") 91 | lightmappedGenericShaderMap["normalSampler"] = renderer.lightmappedGenericShader.GetUniform("normalSampler") 92 | lightmappedGenericShaderMap["useLightmap"] = renderer.lightmappedGenericShader.GetUniform("useLightmap") 93 | lightmappedGenericShaderMap["lightmapTextureSampler"] = renderer.lightmappedGenericShader.GetUniform("lightmapTextureSampler") 94 | renderer.uniformMap[renderer.lightmappedGenericShader.Id()] = lightmappedGenericShaderMap 95 | 96 | gosigl.SetLineWidth(32) 97 | gosigl.EnableBlend() 98 | gosigl.EnableDepthTest() 99 | gosigl.EnableCullFace(gosigl.Back, gosigl.WindingClockwise) 100 | 101 | gosigl.ClearColour(0, 0, 0, 1) 102 | } 103 | 104 | var numCalls = 0 105 | 106 | // Called at the start of a frame 107 | func (renderer *Renderer) StartFrame(cam *entity.Camera) { 108 | renderer.matrices.projection = cam.ProjectionMatrix() 109 | renderer.matrices.view = cam.ViewMatrix() 110 | renderer.activeCamera = cam 111 | renderer.viewFrustum = camera.FrustumFromCamera(renderer.activeCamera) 112 | 113 | // Sky 114 | renderer.skyShader.UseProgram() 115 | renderer.setShader(renderer.skyShader.Id()) 116 | opengl.UniformMatrix4fv(renderer.uniformMap[renderer.skyShader.Id()]["projection"], 1, false, &renderer.matrices.projection[0]) 117 | opengl.UniformMatrix4fv(renderer.uniformMap[renderer.skyShader.Id()]["view"], 1, false, &renderer.matrices.view[0]) 118 | 119 | renderer.lightmappedGenericShader.UseProgram() 120 | renderer.setShader(renderer.lightmappedGenericShader.Id()) 121 | 122 | //matrices 123 | opengl.UniformMatrix4fv(renderer.uniformMap[renderer.lightmappedGenericShader.Id()]["projection"], 1, false, &renderer.matrices.projection[0]) 124 | opengl.UniformMatrix4fv(renderer.uniformMap[renderer.lightmappedGenericShader.Id()]["view"], 1, false, &renderer.matrices.view[0]) 125 | 126 | gosigl.Clear(gosigl.MaskColourBufferBit, gosigl.MaskDepthBufferBit) 127 | } 128 | 129 | // Called at the end of a frame 130 | func (renderer *Renderer) EndFrame() { 131 | //if glError := opengl.GetError(); glError != opengl.NO_ERROR { 132 | // logger.Error("error: %d\n", glError) 133 | //} 134 | //logger.Notice("Calls: %d", numCalls) 135 | numCalls = 0 136 | } 137 | 138 | // Draw the main bsp world 139 | func (renderer *Renderer) DrawBsp(world *world.World) { 140 | if bsp.MapGPUResource == nil { 141 | return 142 | } 143 | 144 | modelMatrix := mgl32.Ident4() 145 | opengl.UniformMatrix4fv(renderer.uniformMap[renderer.currentShaderId]["model"], 1, false, &modelMatrix[0]) 146 | gosigl.BindMesh(bsp.MapGPUResource) 147 | 148 | renderClusters := make([]*model.ClusterLeaf, 0) 149 | 150 | for idx, cluster := range world.VisibleClusters() { 151 | // test cluster visibility for this frame 152 | if !renderer.viewFrustum.IsCuboidInFrustum(cluster.Mins, cluster.Maxs) { 153 | continue 154 | } 155 | renderClusters = append(renderClusters, world.VisibleClusters()[idx]) 156 | for _, face := range cluster.Faces { 157 | renderer.DrawFace(&face) 158 | } 159 | } 160 | 161 | // Render objects that dont seem to belong to a cluster 162 | for _, face := range world.Bsp().DefaultCluster().Faces { 163 | renderer.DrawFace(&face) 164 | } 165 | for _, cluster := range renderClusters { 166 | // This is a performance cheat. We measure from the cluster origin for staticProp fades, rather than staticProp origin 167 | distToCluster := float32(math.Sqrt( 168 | math.Pow(float64(cluster.Origin.X()-renderer.activeCamera.Transform().Position.X()), 2) + 169 | math.Pow(float64(cluster.Origin.Y()-renderer.activeCamera.Transform().Position.Y()), 2) + 170 | math.Pow(float64(cluster.Origin.Z()-renderer.activeCamera.Transform().Position.Z()), 2))) 171 | 172 | for _, staticProp := range cluster.StaticProps { 173 | // Skip render if staticProp is fully faded 174 | if staticProp.FadeMaxDistance() > 0 && distToCluster >= staticProp.FadeMaxDistance() { 175 | continue 176 | } 177 | renderer.DrawModel(staticProp.Model(), staticProp.Transform().TransformationMatrix()) 178 | } 179 | } 180 | for _, prop := range world.Bsp().DefaultCluster().StaticProps { 181 | renderer.DrawModel(prop.Model(), prop.Transform().TransformationMatrix()) 182 | } 183 | } 184 | 185 | // Draw skybox (bsp model, staticprops, sky material) 186 | func (renderer *Renderer) DrawSkybox(sky *world.Sky) { 187 | if sky == nil || bsp.MapGPUResource == nil { 188 | return 189 | } 190 | 191 | if sky.GetVisibleBsp() != nil { 192 | modelMatrix := sky.Transform().TransformationMatrix() 193 | opengl.UniformMatrix4fv(renderer.uniformMap[renderer.currentShaderId]["model"], 1, false, &modelMatrix[0]) 194 | 195 | gosigl.BindMesh(bsp.MapGPUResource) 196 | //renderer.BindMesh(sky.GetVisibleBsp().Mesh()) 197 | for _, cluster := range sky.GetClusterLeafs() { 198 | for _, face := range cluster.Faces { 199 | renderer.DrawFace(&face) 200 | } 201 | } 202 | for _, cluster := range sky.GetClusterLeafs() { 203 | for _, prop := range cluster.StaticProps { 204 | renderer.DrawModel(prop.Model(), prop.Transform().TransformationMatrix()) 205 | } 206 | } 207 | } 208 | 209 | //renderer.DrawSkyMaterial(sky.GetCubemap()) 210 | } 211 | 212 | // Render a mesh and its submeshes/primitives 213 | func (renderer *Renderer) DrawModel(model *model.Model, transform mgl32.Mat4) { 214 | opengl.UniformMatrix4fv(renderer.uniformMap[renderer.currentShaderId]["model"], 1, false, &transform[0]) 215 | modelBinding := prop.ModelIdMap[model.FilePath()] 216 | if modelBinding == nil { 217 | return 218 | } 219 | for idx, mesh := range model.Meshes() { 220 | // Missing materials will be flat coloured 221 | if mesh == nil || mesh.Material() == nil { 222 | // We need the fallback material 223 | continue 224 | } 225 | renderer.BindMesh(mesh, modelBinding[idx]) 226 | gosigl.DrawArray(0, len(mesh.Vertices())/3) 227 | 228 | numCalls++ 229 | } 230 | } 231 | 232 | func (renderer *Renderer) BindMesh(target mesh.IMesh, meshBinding *gosigl.VertexObject) { 233 | gosigl.BindMesh(meshBinding) 234 | //target.Bind() 235 | // $basetexture 236 | if target.Material() != nil { 237 | mat := target.Material().(*material.Material) 238 | opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["albedoSampler"], 0) 239 | gosigl.BindTexture2D(gosigl.TextureSlot(0), renderer.materialCache.FetchCachedTexture(mat.Textures.Albedo.FilePath())) 240 | 241 | if mat.Textures.Normal != nil { 242 | opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["normalSampler"], 1) 243 | gosigl.BindTexture2D(gosigl.TextureSlot(1), renderer.materialCache.FetchCachedTexture(mat.Textures.Normal.FilePath())) 244 | } 245 | } 246 | // Bind lightmap texture if it exists 247 | //if target.GetLightmap() != nil { 248 | // opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["useLightmap"], 0) // lightmaps disabled 249 | // opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["lightmapTextureSampler"], 2) 250 | // //target.GetLightmap().Bind() 251 | //} else { 252 | opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["useLightmap"], 0) 253 | //} 254 | } 255 | 256 | func (renderer *Renderer) DrawFace(target *mesh.Face) { 257 | // Skip materialless faces 258 | if target.Material() == nil { 259 | return 260 | } 261 | 262 | // $basetexture 263 | mat := target.Material().(*material.Material) 264 | opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["albedoSampler"], 0) 265 | gosigl.BindTexture2D(gosigl.TextureSlot(0), renderer.materialCache.FetchCachedTexture(mat.Textures.Albedo.FilePath())) 266 | 267 | if mat.Textures.Normal != nil { 268 | opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["normalSampler"], 1) 269 | gosigl.BindTexture2D(gosigl.TextureSlot(1), renderer.materialCache.FetchCachedTexture(mat.Textures.Normal.FilePath())) 270 | } 271 | 272 | // Bind lightmap texture if it exists 273 | //if target.IsLightmapped() { 274 | // opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["useLightmap"], 0) // lightmaps disabled 275 | // opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["lightmapTextureSampler"], 2) 276 | // //target.Lightmap().Bind() 277 | //} else { 278 | opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["useLightmap"], 0) 279 | //} 280 | gosigl.DrawArray(int(target.Offset()), int(target.Length())) 281 | } 282 | 283 | // Render the sky material 284 | func (renderer *Renderer) DrawSkyMaterial(skybox *model.Model) { 285 | if skybox == nil { 286 | return 287 | } 288 | var oldCullFaceMode int32 289 | opengl.GetIntegerv(opengl.CULL_FACE_MODE, &oldCullFaceMode) 290 | var oldDepthFuncMode int32 291 | opengl.GetIntegerv(opengl.DEPTH_FUNC, &oldDepthFuncMode) 292 | 293 | opengl.CullFace(opengl.FRONT) 294 | opengl.DepthFunc(opengl.LEQUAL) 295 | opengl.DepthMask(false) 296 | 297 | renderer.skyShader.UseProgram() 298 | renderer.setShader(renderer.skyShader.Id()) 299 | opengl.UniformMatrix4fv(renderer.uniformMap[renderer.skyShader.Id()]["projection"], 1, false, &renderer.matrices.projection[0]) 300 | opengl.UniformMatrix4fv(renderer.uniformMap[renderer.skyShader.Id()]["view"], 1, false, &renderer.matrices.view[0]) 301 | 302 | //DRAW 303 | //skybox.GetMeshes()[0].Bind() 304 | //skybox.GetMeshes()[0].GetMaterial().Bind() 305 | opengl.Uniform1i(renderer.uniformMap[renderer.currentShaderId]["cubemapSampler"], 0) 306 | renderer.DrawModel(skybox, mgl32.Ident4()) 307 | 308 | // End 309 | opengl.DepthMask(true) 310 | opengl.CullFace(uint32(oldCullFaceMode)) 311 | opengl.DepthFunc(uint32(oldDepthFuncMode)) 312 | 313 | // Back to default shader 314 | renderer.lightmappedGenericShader.UseProgram() 315 | renderer.setShader(renderer.lightmappedGenericShader.Id()) 316 | } 317 | 318 | func (renderer *Renderer) setShader(shader uint32) { 319 | if renderer.currentShaderId != shader { 320 | renderer.currentShaderId = shader 321 | } 322 | } 323 | 324 | func (renderer *Renderer) Unregister() { 325 | renderer.skyShader.Destroy() 326 | renderer.lightmappedGenericShader.Destroy() 327 | } 328 | 329 | func NewRenderer() *Renderer { 330 | return &Renderer{ 331 | uniformMap: map[uint32]map[string]int32{}, 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /renderer/gl/shaders/fragment.go: -------------------------------------------------------------------------------- 1 | package shaders 2 | 3 | //language=glsl 4 | var Fragment = ` 5 | #version 410 6 | 7 | uniform int useLightmap; 8 | 9 | uniform sampler2D albedoSampler; 10 | uniform sampler2D normalSampler; 11 | uniform sampler2D lightmapTextureSampler; 12 | 13 | in vec2 UV; 14 | in vec3 EyeDirection; 15 | in vec3 LightDirection; 16 | 17 | out vec4 frag_colour; 18 | 19 | vec4 GetAlbedo(in sampler2D sampler, in vec2 uv) 20 | { 21 | return texture(sampler, uv).rgba; 22 | } 23 | 24 | float CalculateNormalFactor(in sampler2D sampler, vec2 uv) 25 | { 26 | vec3 L = normalize(LightDirection); 27 | vec3 N = normalize(texture(sampler, uv).xyz * 2.0 - 1.0); // transform coordinates to -1-1 from 0-1 28 | 29 | return max(dot(N,L), 0.0); 30 | } 31 | 32 | vec4 GetSpecular(in sampler2D normalSampler, vec2 uv) { 33 | vec3 N = normalize(texture(normalSampler, uv).xyz * 2.0 - 1.0); // transform coordinates to -1-1 from 0-1 34 | vec3 L = normalize(LightDirection); 35 | vec3 R = reflect(-L, N); 36 | vec3 V = normalize(EyeDirection); 37 | 38 | // replace with texture where relevant 39 | vec3 specularSample = vec3(1.0); 40 | 41 | return max(pow(dot(R, V), 5.0), 0.0) * vec4(specularSample.xyz, 1.0); 42 | } 43 | 44 | // Lightmaps the face 45 | // Does nothing if lightmap was not defined 46 | vec4 GetLightmap(in sampler2D lightmap, in vec2 uv) 47 | { 48 | return texture(lightmap, uv).rgba; 49 | } 50 | 51 | void main() 52 | { 53 | float bumpFactor = CalculateNormalFactor(normalSampler, UV); 54 | vec4 diffuse = GetAlbedo(albedoSampler, UV); 55 | 56 | vec4 specular = GetSpecular(normalSampler, UV); 57 | 58 | frag_colour = diffuse + specular; 59 | } 60 | ` + "\x00" 61 | -------------------------------------------------------------------------------- /renderer/gl/shaders/sky/fragment.go: -------------------------------------------------------------------------------- 1 | package sky 2 | 3 | // language=glsl 4 | var Fragment = ` 5 | #version 410 6 | 7 | in vec3 UV; 8 | 9 | out vec4 frag_colour; 10 | 11 | uniform samplerCube cubemapSample; 12 | 13 | void main() { 14 | // Output color = color of the texture at the specified UV 15 | frag_colour = texture( cubemapSample, UV ); 16 | } 17 | ` + "\x00" 18 | -------------------------------------------------------------------------------- /renderer/gl/shaders/sky/vertex.go: -------------------------------------------------------------------------------- 1 | package sky 2 | 3 | // language=glsl 4 | var Vertex = ` 5 | #version 410 6 | 7 | uniform mat4 projection; 8 | uniform mat4 view; 9 | uniform mat4 model; 10 | 11 | layout(location = 0) in vec3 vertexPosition; 12 | layout(location = 1) in vec2 vertexUV; 13 | layout(location = 2) in vec2 vertexNormal; 14 | 15 | out vec3 UV; 16 | 17 | void main() { 18 | vec4 WVP_Pos = (projection * view * model) * vec4(vertexPosition, 1.0); 19 | gl_Position = WVP_Pos.xyww; 20 | UV = vertexPosition; 21 | } 22 | ` + "\x00" 23 | -------------------------------------------------------------------------------- /renderer/gl/shaders/vertex.go: -------------------------------------------------------------------------------- 1 | package shaders 2 | 3 | // language=glsl 4 | var Vertex = ` 5 | #version 410 6 | 7 | uniform mat4 projection; 8 | uniform mat4 view; 9 | uniform mat4 model; 10 | 11 | layout(location = 0) in vec3 vertexPosition; 12 | layout(location = 1) in vec2 vertexUV; 13 | layout(location = 2) in vec3 vertexNormal; 14 | layout(location = 3) in vec4 vertexTangent; 15 | layout(location = 4) in vec2 lightmapUV; 16 | 17 | out vec2 UV; 18 | out vec3 EyeDirection; 19 | out vec3 LightDirection; 20 | 21 | // temporary 22 | uniform vec3 lightPos = vec3(0.0, 0.0, 100.0); 23 | 24 | void calculateEyePosition() { 25 | // View space vertex position 26 | vec4 P = view * model * vec4(vertexPosition, 1.0); 27 | // Normal vector 28 | vec3 N = normalize(mat3(view * model) * vertexNormal); 29 | // Tangent vector 30 | vec3 T = normalize(mat3(view * model) * vertexTangent.xyz); 31 | // Bitangent vector 32 | vec3 B = cross(N, T); 33 | // Vector from target to viewer 34 | vec3 V = -P.xyz; 35 | 36 | EyeDirection = normalize(vec3(dot(V, T), dot(V, B), dot(V, N))); 37 | 38 | vec3 L = lightPos - P.xyz; 39 | LightDirection = normalize(vec3(dot(L, T), dot(L, B), dot(L, N))); 40 | } 41 | 42 | void main() { 43 | gl_Position = projection * view * model * vec4(vertexPosition, 1.0); 44 | 45 | UV = vertexUV; 46 | 47 | // bump + specular related 48 | calculateEyePosition(); 49 | } 50 | ` + "\x00" 51 | -------------------------------------------------------------------------------- /renderer/irenderer.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/scene/world" 5 | "github.com/galaco/lambda-core/entity" 6 | "github.com/galaco/lambda-core/model" 7 | "github.com/go-gl/mathgl/mgl32" 8 | ) 9 | 10 | type IRenderer interface { 11 | StartFrame(*entity.Camera) 12 | LoadShaders() 13 | DrawBsp(*world.World) 14 | DrawSkybox(*world.Sky) 15 | DrawModel(*model.Model, mgl32.Mat4) 16 | DrawSkyMaterial(*model.Model) 17 | EndFrame() 18 | Unregister() 19 | } 20 | -------------------------------------------------------------------------------- /renderer/manager.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/engine" 5 | "github.com/galaco/lambda-client/renderer/cache" 6 | "github.com/galaco/lambda-client/renderer/gl" 7 | "github.com/galaco/lambda-client/renderer/ui" 8 | "github.com/galaco/lambda-client/scene" 9 | "github.com/galaco/lambda-core/vgui" 10 | "github.com/galaco/tinygametools" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | type Manager struct { 16 | engine.Manager 17 | 18 | window *tinygametools.Window 19 | 20 | renderer IRenderer 21 | uiRenderer ui.Renderer 22 | 23 | dynamicPropCache cache.PropCache 24 | } 25 | 26 | var cacheMutex sync.Mutex 27 | 28 | func (manager *Manager) Register() { 29 | manager.renderer = gl.NewRenderer() 30 | 31 | manager.renderer.LoadShaders() 32 | 33 | //manager.uiRenderer.InitRenderContext(manager.window) 34 | } 35 | 36 | func (manager *Manager) Update(dt float64) { 37 | currentScene := scene.Get() 38 | 39 | if manager.dynamicPropCache.NeedsRecache() { 40 | manager.RecacheEntities(currentScene) 41 | } 42 | 43 | currentScene.CurrentCamera().Update(dt) 44 | currentScene.GetWorld().TestVisibility(currentScene.CurrentCamera().Transform().Position) 45 | 46 | renderableWorld := currentScene.GetWorld() 47 | 48 | // Begin actual rendering 49 | manager.renderer.StartFrame(currentScene.CurrentCamera()) 50 | 51 | // Start with sky 52 | manager.renderer.DrawSkybox(renderableWorld.Sky()) 53 | 54 | // Draw static world first 55 | manager.renderer.DrawBsp(renderableWorld) 56 | 57 | // Dynamic objects 58 | cacheMutex.Lock() 59 | for _, entry := range *manager.dynamicPropCache.All() { 60 | manager.renderer.DrawModel(entry.Model, entry.Transform.TransformationMatrix()) 61 | } 62 | cacheMutex.Unlock() 63 | 64 | //manager.uiRenderer.DrawUI() 65 | 66 | manager.renderer.EndFrame() 67 | } 68 | 69 | func (manager *Manager) SetUIMasterPanel(panel *vgui.MasterPanel) { 70 | manager.uiRenderer.SetMasterPanel(panel) 71 | } 72 | 73 | func (manager *Manager) RecacheEntities(scene *scene.Scene) { 74 | c := cache.NewPropCache() 75 | go func() { 76 | for _, ent := range *scene.GetAllEntities() { 77 | if ent.KeyValues().ValueForKey("model") == "" { 78 | continue 79 | } 80 | m := ent.KeyValues().ValueForKey("model") 81 | // Its a brush entity 82 | if !strings.HasSuffix(m, ".mdl") { 83 | continue 84 | } 85 | // Its a point entity 86 | c.Add(ent) 87 | } 88 | 89 | cacheMutex.Lock() 90 | manager.dynamicPropCache = *c 91 | cacheMutex.Unlock() 92 | }() 93 | } 94 | 95 | func NewRenderManager(win *tinygametools.Window) *Manager { 96 | return &Manager{ 97 | window: win, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /renderer/ui/imgui-go_impl3.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "math" 5 | "unsafe" 6 | 7 | "github.com/go-gl/gl/v4.1-core/gl" 8 | "github.com/go-gl/glfw/v3.2/glfw" 9 | "github.com/inkyblackness/imgui-go" 10 | ) 11 | 12 | type imguiGlfw3 struct { 13 | window *glfw.Window 14 | time float64 15 | mouseJustPressed [3]bool 16 | 17 | glslVersion string 18 | fontTexture uint32 19 | shaderHandle uint32 20 | vertHandle uint32 21 | fragHandle uint32 22 | attribLocationTex int32 23 | attribLocationProjMtx int32 24 | attribLocationPosition int32 25 | attribLocationUV int32 26 | attribLocationColor int32 27 | vboHandle uint32 28 | elementsHandle uint32 29 | } 30 | 31 | func imguiGlfw3Init(window *glfw.Window) *imguiGlfw3 { 32 | impl := &imguiGlfw3{ 33 | window: window, 34 | glslVersion: "#version 150", 35 | } 36 | 37 | io := imgui.CurrentIO() 38 | // Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array. 39 | io.KeyMap(int(imgui.KeyTab), int(glfw.KeyTab)) 40 | io.KeyMap(int(imgui.KeyLeftArrow), int(glfw.KeyLeft)) 41 | io.KeyMap(int(imgui.KeyRightArrow), int(glfw.KeyRight)) 42 | io.KeyMap(int(imgui.KeyUpArrow), int(glfw.KeyUp)) 43 | io.KeyMap(int(imgui.KeyDownArrow), int(glfw.KeyDown)) 44 | io.KeyMap(int(imgui.KeyPageUp), int(glfw.KeyPageUp)) 45 | io.KeyMap(int(imgui.KeyPageDown), int(glfw.KeyPageDown)) 46 | io.KeyMap(int(imgui.KeyHome), int(glfw.KeyHome)) 47 | io.KeyMap(int(imgui.KeyEnd), int(glfw.KeyEnd)) 48 | io.KeyMap(int(imgui.KeyInsert), int(glfw.KeyInsert)) 49 | io.KeyMap(int(imgui.KeyDelete), int(glfw.KeyDelete)) 50 | io.KeyMap(int(imgui.KeyBackspace), int(glfw.KeyBackspace)) 51 | io.KeyMap(int(imgui.KeySpace), int(glfw.KeySpace)) 52 | io.KeyMap(int(imgui.KeyEnter), int(glfw.KeyEnter)) 53 | io.KeyMap(int(imgui.KeyEscape), int(glfw.KeyEscape)) 54 | io.KeyMap(int(imgui.KeyA), int(glfw.KeyA)) 55 | io.KeyMap(int(imgui.KeyC), int(glfw.KeyC)) 56 | io.KeyMap(int(imgui.KeyV), int(glfw.KeyV)) 57 | io.KeyMap(int(imgui.KeyX), int(glfw.KeyX)) 58 | io.KeyMap(int(imgui.KeyY), int(glfw.KeyY)) 59 | io.KeyMap(int(imgui.KeyZ), int(glfw.KeyZ)) 60 | 61 | impl.installCallbacks() 62 | 63 | return impl 64 | } 65 | 66 | func (impl *imguiGlfw3) NewFrame() { 67 | if impl.fontTexture == 0 { 68 | impl.createDeviceObjects() 69 | } 70 | 71 | io := imgui.CurrentIO() 72 | // Setup display size (every frame to accommodate for window resizing) 73 | windowWidth, windowHeight := impl.window.GetSize() 74 | io.SetDisplaySize(imgui.Vec2{X: float32(windowWidth), Y: float32(windowHeight)}) 75 | 76 | // Setup time step 77 | currentTime := glfw.GetTime() 78 | if impl.time > 0 { 79 | io.SetDeltaTime(float32(currentTime - impl.time)) 80 | } 81 | impl.time = currentTime 82 | 83 | // Setup inputs 84 | if impl.window.GetAttrib(glfw.Focused) != 0 { 85 | x, y := impl.window.GetCursorPos() 86 | io.SetMousePosition(imgui.Vec2{X: float32(x), Y: float32(y)}) 87 | } else { 88 | io.SetMousePosition(imgui.Vec2{X: -math.MaxFloat32, Y: -math.MaxFloat32}) 89 | } 90 | 91 | for i := 0; i < len(impl.mouseJustPressed); i++ { 92 | down := impl.mouseJustPressed[i] || (impl.window.GetMouseButton(buttonIDByIndex[i]) == glfw.Press) 93 | io.SetMouseButtonDown(i, down) 94 | impl.mouseJustPressed[i] = false 95 | } 96 | 97 | imgui.NewFrame() 98 | } 99 | 100 | func (impl *imguiGlfw3) createDeviceObjects() { 101 | // Backup GL state 102 | var lastTexture int32 103 | var lastArrayBuffer int32 104 | var lastVertexArray int32 105 | gl.GetIntegerv(gl.TEXTURE_BINDING_2D, &lastTexture) 106 | gl.GetIntegerv(gl.ARRAY_BUFFER_BINDING, &lastArrayBuffer) 107 | gl.GetIntegerv(gl.VERTEX_ARRAY_BINDING, &lastVertexArray) 108 | 109 | vertexShader := impl.glslVersion + ` 110 | uniform mat4 ProjMtx; 111 | in vec2 Position; 112 | in vec2 UV; 113 | in vec4 Color; 114 | out vec2 Frag_UV; 115 | out vec4 Frag_Color; 116 | void main() 117 | { 118 | Frag_UV = UV; 119 | Frag_Color = Color; 120 | gl_Position = ProjMtx * vec4(Position.xy,0,1); 121 | } 122 | ` 123 | fragmentShader := impl.glslVersion + ` 124 | uniform sampler2D Texture; 125 | in vec2 Frag_UV; 126 | in vec4 Frag_Color; 127 | out vec4 Out_Color; 128 | void main() 129 | { 130 | Out_Color = vec4(Frag_Color.rgb, Frag_Color.a * texture( Texture, Frag_UV.st).r); 131 | } 132 | ` 133 | impl.shaderHandle = gl.CreateProgram() 134 | impl.vertHandle = gl.CreateShader(gl.VERTEX_SHADER) 135 | impl.fragHandle = gl.CreateShader(gl.FRAGMENT_SHADER) 136 | 137 | glShaderSource := func(handle uint32, source string) { 138 | csource, free := gl.Strs(source + "\x00") 139 | defer free() 140 | 141 | gl.ShaderSource(handle, 1, csource, nil) 142 | } 143 | 144 | glShaderSource(impl.vertHandle, vertexShader) 145 | glShaderSource(impl.fragHandle, fragmentShader) 146 | gl.CompileShader(impl.vertHandle) 147 | gl.CompileShader(impl.fragHandle) 148 | gl.AttachShader(impl.shaderHandle, impl.vertHandle) 149 | gl.AttachShader(impl.shaderHandle, impl.fragHandle) 150 | gl.LinkProgram(impl.shaderHandle) 151 | 152 | impl.attribLocationTex = gl.GetUniformLocation(impl.shaderHandle, gl.Str("Texture"+"\x00")) 153 | impl.attribLocationProjMtx = gl.GetUniformLocation(impl.shaderHandle, gl.Str("ProjMtx"+"\x00")) 154 | impl.attribLocationPosition = gl.GetAttribLocation(impl.shaderHandle, gl.Str("Position"+"\x00")) 155 | impl.attribLocationUV = gl.GetAttribLocation(impl.shaderHandle, gl.Str("UV"+"\x00")) 156 | impl.attribLocationColor = gl.GetAttribLocation(impl.shaderHandle, gl.Str("Color"+"\x00")) 157 | 158 | gl.GenBuffers(1, &impl.vboHandle) 159 | gl.GenBuffers(1, &impl.elementsHandle) 160 | 161 | impl.createFontsTexture() 162 | 163 | // Restore modified GL state 164 | gl.BindTexture(gl.TEXTURE_2D, uint32(lastTexture)) 165 | gl.BindBuffer(gl.ARRAY_BUFFER, uint32(lastArrayBuffer)) 166 | gl.BindVertexArray(uint32(lastVertexArray)) 167 | } 168 | 169 | func (impl *imguiGlfw3) createFontsTexture() { 170 | // Build texture atlas 171 | io := imgui.CurrentIO() 172 | image := io.Fonts().TextureDataAlpha8() 173 | 174 | // Upload texture to graphics system 175 | var lastTexture int32 176 | gl.GetIntegerv(gl.TEXTURE_BINDING_2D, &lastTexture) 177 | gl.GenTextures(1, &impl.fontTexture) 178 | gl.BindTexture(gl.TEXTURE_2D, impl.fontTexture) 179 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) 180 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) 181 | gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0) 182 | gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(image.Width), int32(image.Height), 183 | 0, gl.RED, gl.UNSIGNED_BYTE, image.Pixels) 184 | 185 | // Store our identifier 186 | io.Fonts().SetTextureID(imgui.TextureID(impl.fontTexture)) 187 | 188 | // Restore state 189 | gl.BindTexture(gl.TEXTURE_2D, uint32(lastTexture)) 190 | } 191 | 192 | func (impl *imguiGlfw3) Render(drawData imgui.DrawData) { 193 | // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) 194 | displayWidth, displayHeight := impl.window.GetSize() 195 | fbWidth, fbHeight := impl.window.GetFramebufferSize() 196 | if (fbWidth <= 0) || (fbHeight <= 0) { 197 | return 198 | } 199 | drawData.ScaleClipRects(imgui.Vec2{ 200 | X: float32(fbWidth) / float32(displayWidth), 201 | Y: float32(fbHeight) / float32(displayHeight), 202 | }) 203 | 204 | // Backup GL state 205 | var lastActiveTexture int32 206 | gl.GetIntegerv(gl.ACTIVE_TEXTURE, &lastActiveTexture) 207 | gl.ActiveTexture(gl.TEXTURE0) 208 | var lastProgram int32 209 | gl.GetIntegerv(gl.CURRENT_PROGRAM, &lastProgram) 210 | var lastTexture int32 211 | gl.GetIntegerv(gl.TEXTURE_BINDING_2D, &lastTexture) 212 | var lastSampler int32 213 | gl.GetIntegerv(gl.SAMPLER_BINDING, &lastSampler) 214 | var lastArrayBuffer int32 215 | gl.GetIntegerv(gl.ARRAY_BUFFER_BINDING, &lastArrayBuffer) 216 | var lastElementArrayBuffer int32 217 | gl.GetIntegerv(gl.ELEMENT_ARRAY_BUFFER_BINDING, &lastElementArrayBuffer) 218 | var lastVertexArray int32 219 | gl.GetIntegerv(gl.VERTEX_ARRAY_BINDING, &lastVertexArray) 220 | var lastPolygonMode [2]int32 221 | gl.GetIntegerv(gl.POLYGON_MODE, &lastPolygonMode[0]) 222 | var lastViewport [4]int32 223 | gl.GetIntegerv(gl.VIEWPORT, &lastViewport[0]) 224 | var lastScissorBox [4]int32 225 | gl.GetIntegerv(gl.SCISSOR_BOX, &lastScissorBox[0]) 226 | var lastBlendSrcRgb int32 227 | gl.GetIntegerv(gl.BLEND_SRC_RGB, &lastBlendSrcRgb) 228 | var lastBlendDstRgb int32 229 | gl.GetIntegerv(gl.BLEND_DST_RGB, &lastBlendDstRgb) 230 | var lastBlendSrcAlpha int32 231 | gl.GetIntegerv(gl.BLEND_SRC_ALPHA, &lastBlendSrcAlpha) 232 | var lastBlendDstAlpha int32 233 | gl.GetIntegerv(gl.BLEND_DST_ALPHA, &lastBlendDstAlpha) 234 | var lastBlendEquationRgb int32 235 | gl.GetIntegerv(gl.BLEND_EQUATION_RGB, &lastBlendEquationRgb) 236 | var lastBlendEquationAlpha int32 237 | gl.GetIntegerv(gl.BLEND_EQUATION_ALPHA, &lastBlendEquationAlpha) 238 | lastEnableBlend := gl.IsEnabled(gl.BLEND) 239 | lastEnableCullFace := gl.IsEnabled(gl.CULL_FACE) 240 | lastEnableDepthTest := gl.IsEnabled(gl.DEPTH_TEST) 241 | lastEnableScissorTest := gl.IsEnabled(gl.SCISSOR_TEST) 242 | 243 | // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill 244 | gl.Enable(gl.BLEND) 245 | gl.BlendEquation(gl.FUNC_ADD) 246 | gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 247 | gl.Disable(gl.CULL_FACE) 248 | gl.Disable(gl.DEPTH_TEST) 249 | gl.Enable(gl.SCISSOR_TEST) 250 | gl.PolygonMode(gl.FRONT_AND_BACK, gl.FILL) 251 | 252 | // Setup viewport, orthographic projection matrix 253 | gl.Viewport(0, 0, int32(fbWidth), int32(fbHeight)) 254 | orthoProjection := [4][4]float32{ 255 | {2.0 / float32(displayWidth), 0.0, 0.0, 0.0}, 256 | {0.0, 2.0 / float32(-displayHeight), 0.0, 0.0}, 257 | {0.0, 0.0, -1.0, 0.0}, 258 | {-1.0, 1.0, 0.0, 1.0}, 259 | } 260 | gl.UseProgram(impl.shaderHandle) 261 | gl.Uniform1i(impl.attribLocationTex, 0) 262 | gl.UniformMatrix4fv(impl.attribLocationProjMtx, 1, false, &orthoProjection[0][0]) 263 | gl.BindSampler(0, 0) // Rely on combined texture/sampler state. 264 | 265 | // Recreate the VAO every time 266 | // (This is to easily allow multiple GL contexts. VAO are not shared among GL contexts, and 267 | // we don't track creation/deletion of windows so we don't have an obvious key to use to cache them.) 268 | var vaoHandle uint32 269 | gl.GenVertexArrays(1, &vaoHandle) 270 | gl.BindVertexArray(vaoHandle) 271 | gl.BindBuffer(gl.ARRAY_BUFFER, impl.vboHandle) 272 | gl.EnableVertexAttribArray(uint32(impl.attribLocationPosition)) 273 | gl.EnableVertexAttribArray(uint32(impl.attribLocationUV)) 274 | gl.EnableVertexAttribArray(uint32(impl.attribLocationColor)) 275 | vertexSize, vertexOffsetPos, vertexOffsetUv, vertexOffsetCol := imgui.VertexBufferLayout() 276 | gl.VertexAttribPointer(uint32(impl.attribLocationPosition), 2, gl.FLOAT, false, int32(vertexSize), unsafe.Pointer(uintptr(vertexOffsetPos))) 277 | gl.VertexAttribPointer(uint32(impl.attribLocationUV), 2, gl.FLOAT, false, int32(vertexSize), unsafe.Pointer(uintptr(vertexOffsetUv))) 278 | gl.VertexAttribPointer(uint32(impl.attribLocationColor), 4, gl.UNSIGNED_BYTE, true, int32(vertexSize), unsafe.Pointer(uintptr(vertexOffsetCol))) 279 | indexSize := imgui.IndexBufferLayout() 280 | drawType := gl.UNSIGNED_SHORT 281 | if indexSize == 4 { 282 | drawType = gl.UNSIGNED_INT 283 | } 284 | 285 | // Draw 286 | for _, list := range drawData.CommandLists() { 287 | var indexBufferOffset uintptr 288 | 289 | vertexBuffer, vertexBufferSize := list.VertexBuffer() 290 | gl.BindBuffer(gl.ARRAY_BUFFER, impl.vboHandle) 291 | gl.BufferData(gl.ARRAY_BUFFER, vertexBufferSize, vertexBuffer, gl.STREAM_DRAW) 292 | 293 | indexBuffer, indexBufferSize := list.IndexBuffer() 294 | gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, impl.elementsHandle) 295 | gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, indexBufferSize, indexBuffer, gl.STREAM_DRAW) 296 | 297 | for _, cmd := range list.Commands() { 298 | if cmd.HasUserCallback() { 299 | cmd.CallUserCallback(list) 300 | } else { 301 | gl.BindTexture(gl.TEXTURE_2D, uint32(cmd.TextureID())) 302 | clipRect := cmd.ClipRect() 303 | gl.Scissor(int32(clipRect.X), int32(fbHeight)-int32(clipRect.W), int32(clipRect.Z-clipRect.X), int32(clipRect.W-clipRect.Y)) 304 | gl.DrawElements(gl.TRIANGLES, int32(cmd.ElementCount()), uint32(drawType), unsafe.Pointer(indexBufferOffset)) 305 | } 306 | indexBufferOffset += uintptr(cmd.ElementCount() * indexSize) 307 | } 308 | } 309 | gl.DeleteVertexArrays(1, &vaoHandle) 310 | 311 | // Restore modified GL state 312 | gl.UseProgram(uint32(lastProgram)) 313 | gl.BindTexture(gl.TEXTURE_2D, uint32(lastTexture)) 314 | gl.BindSampler(0, uint32(lastSampler)) 315 | gl.ActiveTexture(uint32(lastActiveTexture)) 316 | gl.BindVertexArray(uint32(lastVertexArray)) 317 | gl.BindBuffer(gl.ARRAY_BUFFER, uint32(lastArrayBuffer)) 318 | gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, uint32(lastElementArrayBuffer)) 319 | gl.BlendEquationSeparate(uint32(lastBlendEquationRgb), uint32(lastBlendEquationAlpha)) 320 | gl.BlendFuncSeparate(uint32(lastBlendSrcRgb), uint32(lastBlendDstRgb), uint32(lastBlendSrcAlpha), uint32(lastBlendDstAlpha)) 321 | if lastEnableBlend { 322 | gl.Enable(gl.BLEND) 323 | } else { 324 | gl.Disable(gl.BLEND) 325 | } 326 | if lastEnableCullFace { 327 | gl.Enable(gl.CULL_FACE) 328 | } else { 329 | gl.Disable(gl.CULL_FACE) 330 | } 331 | if lastEnableDepthTest { 332 | gl.Enable(gl.DEPTH_TEST) 333 | } else { 334 | gl.Disable(gl.DEPTH_TEST) 335 | } 336 | if lastEnableScissorTest { 337 | gl.Enable(gl.SCISSOR_TEST) 338 | } else { 339 | gl.Disable(gl.SCISSOR_TEST) 340 | } 341 | gl.PolygonMode(gl.FRONT_AND_BACK, uint32(lastPolygonMode[0])) 342 | gl.Viewport(lastViewport[0], lastViewport[1], lastViewport[2], lastViewport[3]) 343 | gl.Scissor(lastScissorBox[0], lastScissorBox[1], lastScissorBox[2], lastScissorBox[3]) 344 | } 345 | 346 | func (impl *imguiGlfw3) Shutdown() { 347 | impl.invalidateDeviceObjects() 348 | } 349 | 350 | func (impl *imguiGlfw3) invalidateDeviceObjects() { 351 | if impl.vboHandle != 0 { 352 | gl.DeleteBuffers(1, &impl.vboHandle) 353 | } 354 | impl.vboHandle = 0 355 | if impl.elementsHandle != 0 { 356 | gl.DeleteBuffers(1, &impl.elementsHandle) 357 | } 358 | impl.elementsHandle = 0 359 | 360 | if (impl.shaderHandle != 0) && (impl.vertHandle != 0) { 361 | gl.DetachShader(impl.shaderHandle, impl.vertHandle) 362 | } 363 | if impl.vertHandle != 0 { 364 | gl.DeleteShader(impl.vertHandle) 365 | } 366 | impl.vertHandle = 0 367 | 368 | if (impl.shaderHandle != 0) && (impl.fragHandle != 0) { 369 | gl.DetachShader(impl.shaderHandle, impl.fragHandle) 370 | } 371 | if impl.fragHandle != 0 { 372 | gl.DeleteShader(impl.fragHandle) 373 | } 374 | impl.fragHandle = 0 375 | 376 | if impl.shaderHandle != 0 { 377 | gl.DeleteProgram(impl.shaderHandle) 378 | } 379 | impl.shaderHandle = 0 380 | 381 | if impl.fontTexture != 0 { 382 | gl.DeleteTextures(1, &impl.fontTexture) 383 | imgui.CurrentIO().Fonts().SetTextureID(0) 384 | impl.fontTexture = 0 385 | } 386 | } 387 | 388 | func (impl *imguiGlfw3) installCallbacks() { 389 | impl.window.SetMouseButtonCallback(impl.mouseButtonChange) 390 | impl.window.SetScrollCallback(impl.mouseScrollChange) 391 | impl.window.SetKeyCallback(impl.keyChange) 392 | impl.window.SetCharCallback(impl.charChange) 393 | } 394 | 395 | var buttonIndexByID = map[glfw.MouseButton]int{ 396 | glfw.MouseButton1: 0, 397 | glfw.MouseButton2: 1, 398 | glfw.MouseButton3: 2, 399 | } 400 | 401 | var buttonIDByIndex = map[int]glfw.MouseButton{ 402 | 0: glfw.MouseButton1, 403 | 1: glfw.MouseButton2, 404 | 2: glfw.MouseButton3, 405 | } 406 | 407 | func (impl *imguiGlfw3) mouseButtonChange(window *glfw.Window, rawButton glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) { 408 | buttonIndex, known := buttonIndexByID[rawButton] 409 | 410 | if known && (action == glfw.Press) { 411 | impl.mouseJustPressed[buttonIndex] = true 412 | } 413 | } 414 | 415 | func (impl *imguiGlfw3) mouseScrollChange(window *glfw.Window, x, y float64) { 416 | io := imgui.CurrentIO() 417 | io.AddMouseWheelDelta(float32(x), float32(y)) 418 | } 419 | 420 | func (impl *imguiGlfw3) keyChange(window *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { 421 | io := imgui.CurrentIO() 422 | if action == glfw.Press { 423 | io.KeyPress(int(key)) 424 | } 425 | if action == glfw.Release { 426 | io.KeyRelease(int(key)) 427 | } 428 | 429 | // Modifiers are not reliable across systems 430 | io.KeyCtrl(int(glfw.KeyLeftControl), int(glfw.KeyRightControl)) 431 | io.KeyShift(int(glfw.KeyLeftShift), int(glfw.KeyRightShift)) 432 | io.KeyAlt(int(glfw.KeyLeftAlt), int(glfw.KeyRightAlt)) 433 | io.KeySuper(int(glfw.KeyLeftSuper), int(glfw.KeyRightSuper)) 434 | } 435 | 436 | func (impl *imguiGlfw3) charChange(window *glfw.Window, char rune) { 437 | io := imgui.CurrentIO() 438 | io.AddInputCharacters(string(char)) 439 | } 440 | -------------------------------------------------------------------------------- /renderer/ui/renderer.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/galaco/lambda-core/vgui" 5 | "github.com/galaco/tinygametools" 6 | "github.com/inkyblackness/imgui-go" 7 | ) 8 | 9 | type Renderer struct { 10 | masterPanel *vgui.MasterPanel 11 | imguiImpl *imguiGlfw3 12 | imguiContext *imgui.Context 13 | } 14 | 15 | func (renderer *Renderer) InitRenderContext(win *tinygametools.Window) { 16 | renderer.imguiContext = imgui.CreateContext(nil) 17 | renderer.imguiImpl = imguiGlfw3Init(win.Handle()) 18 | } 19 | 20 | func (renderer *Renderer) SetMasterPanel(ui *vgui.MasterPanel) { 21 | renderer.masterPanel = ui 22 | } 23 | 24 | func (renderer *Renderer) DrawUI() { 25 | renderer.imguiImpl.NewFrame() 26 | imgui.Render() 27 | renderer.imguiImpl.Render(imgui.RenderedDrawData()) 28 | } 29 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /scene/loader.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | bsplib "github.com/galaco/bsp" 5 | "github.com/galaco/bsp/lumps" 6 | "github.com/galaco/lambda-client/internal/config" 7 | "github.com/galaco/lambda-client/scene/visibility" 8 | "github.com/galaco/lambda-client/scene/world" 9 | "github.com/galaco/lambda-core/entity" 10 | "github.com/galaco/lambda-core/lib/util" 11 | "github.com/galaco/lambda-core/loader" 12 | entity2 "github.com/galaco/lambda-core/loader/entity" 13 | "github.com/galaco/lambda-core/model" 14 | entitylib "github.com/galaco/source-tools-common/entity" 15 | "github.com/go-gl/mathgl/mgl32" 16 | "github.com/galaco/filesystem" 17 | ) 18 | 19 | func LoadFromFile(fileName string, fs *filesystem.FileSystem) { 20 | newScene := Get() 21 | 22 | bspData, err := bsplib.ReadFromFile(fileName) 23 | if err != nil { 24 | util.Logger().Panic(err) 25 | } 26 | if bspData.Header().Version < 19 { 27 | util.Logger().Panic("Unsupported BSP Version. Exiting...") 28 | } 29 | 30 | //Set pakfile for filesystem 31 | fs.RegisterPakFile(bspData.Lump(bsplib.LumpPakfile).(*lumps.Pakfile)) 32 | 33 | loadWorld(newScene, bspData, fs) 34 | 35 | loadEntities(newScene, bspData.Lump(bsplib.LumpEntities).(*lumps.EntData), fs) 36 | 37 | loadCamera(newScene) 38 | } 39 | 40 | func loadWorld(targetScene *Scene, file *bsplib.Bsp, fs *filesystem.FileSystem) { 41 | baseWorld := loader.LoadMap(fs, file) 42 | 43 | baseWorldBsp := baseWorld.Bsp() 44 | baseWorldBspFaces := baseWorldBsp.ClusterLeafs()[0].Faces 45 | baseWorldStaticProps := baseWorld.StaticProps() 46 | 47 | visData := visibility.NewVisFromBSP(file) 48 | bspClusters := make([]model.ClusterLeaf, visData.VisibilityLump.NumClusters) 49 | defaultCluster := model.ClusterLeaf{ 50 | Id: 32767, 51 | } 52 | for _, bspLeaf := range visData.Leafs { 53 | for _, leafFace := range visData.LeafFaces[bspLeaf.FirstLeafFace : bspLeaf.FirstLeafFace+bspLeaf.NumLeafFaces] { 54 | if bspLeaf.Cluster == -1 { 55 | //defaultCluster.Faces = append(defaultCluster.Faces, bspFaces[leafFace]) 56 | continue 57 | } 58 | bspClusters[bspLeaf.Cluster].Id = bspLeaf.Cluster 59 | bspClusters[bspLeaf.Cluster].Faces = append(bspClusters[bspLeaf.Cluster].Faces, baseWorldBspFaces[leafFace]) 60 | bspClusters[bspLeaf.Cluster].Mins = mgl32.Vec3{ 61 | float32(bspLeaf.Mins[0]), 62 | float32(bspLeaf.Mins[1]), 63 | float32(bspLeaf.Mins[2]), 64 | } 65 | bspClusters[bspLeaf.Cluster].Maxs = mgl32.Vec3{ 66 | float32(bspLeaf.Maxs[0]), 67 | float32(bspLeaf.Maxs[1]), 68 | float32(bspLeaf.Maxs[2]), 69 | } 70 | bspClusters[bspLeaf.Cluster].Origin = bspClusters[bspLeaf.Cluster].Mins.Add(bspClusters[bspLeaf.Cluster].Maxs.Sub(bspClusters[bspLeaf.Cluster].Mins)) 71 | } 72 | } 73 | 74 | // Assign staticprops to clusters 75 | for idx, prop := range baseWorld.StaticProps() { 76 | for _, leafId := range prop.LeafList() { 77 | clusterId := visData.Leafs[leafId].Cluster 78 | if clusterId == -1 { 79 | defaultCluster.StaticProps = append(defaultCluster.StaticProps, &baseWorldStaticProps[idx]) 80 | continue 81 | } 82 | bspClusters[clusterId].StaticProps = append(bspClusters[clusterId].StaticProps, &baseWorldStaticProps[idx]) 83 | } 84 | } 85 | 86 | for _, idx := range baseWorldBsp.ClusterLeafs()[0].DispFaces { 87 | defaultCluster.Faces = append(defaultCluster.Faces, baseWorldBspFaces[idx]) 88 | } 89 | 90 | baseWorldBsp.SetClusterLeafs(bspClusters) 91 | baseWorldBsp.SetDefaultCluster(defaultCluster) 92 | 93 | targetScene.SetWorld(world.NewWorld(*baseWorld.Bsp(), baseWorld.StaticProps(), visData)) 94 | } 95 | 96 | func loadEntities(targetScene *Scene, entdata *lumps.EntData, fs *filesystem.FileSystem) { 97 | vmfEntityTree, err := entity2.ParseEntities(entdata.GetData()) 98 | if err != nil { 99 | util.Logger().Panic(err) 100 | } 101 | entityList := entitylib.FromVmfNodeTree(vmfEntityTree.Unclassified) 102 | util.Logger().Notice("Found %d entities\n", entityList.Length()) 103 | for i := 0; i < entityList.Length(); i++ { 104 | targetScene.AddEntity(entity2.CreateEntity(entityList.Get(i), fs)) 105 | } 106 | 107 | skyCamera := entityList.FindByKeyValue("classname", "sky_camera") 108 | if skyCamera == nil { 109 | return 110 | } 111 | 112 | worldSpawn := entityList.FindByKeyValue("classname", "worldspawn") 113 | if worldSpawn == nil { 114 | return 115 | } 116 | 117 | targetScene.world.BuildSkybox( 118 | loader.LoadSky(worldSpawn.ValueForKey("skyname"), fs), 119 | skyCamera.VectorForKey("origin"), 120 | float32(skyCamera.IntForKey("scale"))) 121 | } 122 | 123 | func loadCamera(targetScene *Scene) { 124 | targetScene.AddCamera(entity.NewCamera(mgl32.DegToRad(70), float32(config.Get().Video.Width)/float32(config.Get().Video.Height))) 125 | } 126 | -------------------------------------------------------------------------------- /scene/scene.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/scene/world" 5 | "github.com/galaco/lambda-core/entity" 6 | "github.com/galaco/lambda-core/texture" 7 | ) 8 | 9 | type Scene struct { 10 | world *world.World 11 | entities []entity.IEntity 12 | sky *texture.Cubemap 13 | 14 | cameras []entity.Camera 15 | currentCamera *entity.Camera 16 | 17 | isLoaded bool 18 | } 19 | 20 | func (s *Scene) AddEntity(ent entity.IEntity) { 21 | s.entities = append(s.entities, ent) 22 | } 23 | 24 | func (s *Scene) GetEntity(idx int) entity.IEntity { 25 | if idx > len(s.entities) { 26 | return nil 27 | } 28 | return s.entities[idx] 29 | } 30 | 31 | func (s *Scene) FindEntitiesByKey(key string, value string) []entity.IEntity { 32 | ret := make([]entity.IEntity, 0) 33 | for idx, ent := range s.entities { 34 | if ent.KeyValues().ValueForKey(key) == value { 35 | ret = append(ret, s.entities[idx]) 36 | } 37 | } 38 | return ret 39 | } 40 | 41 | func (s *Scene) NumEntities() int { 42 | return len(s.entities) 43 | } 44 | 45 | func (s *Scene) GetAllEntities() *[]entity.IEntity { 46 | return &s.entities 47 | } 48 | 49 | func (s *Scene) SetWorld(world *world.World) { 50 | s.world = world 51 | } 52 | 53 | func (s *Scene) GetWorld() *world.World { 54 | return s.world 55 | } 56 | 57 | func (s *Scene) GetSky() *texture.Cubemap { 58 | return s.sky 59 | } 60 | 61 | func (s *Scene) AddCamera(camera *entity.Camera) { 62 | if s.cameras == nil { 63 | s.cameras = make([]entity.Camera, 0) 64 | } 65 | s.cameras = append(s.cameras, *camera) 66 | 67 | if s.currentCamera == nil { 68 | s.currentCamera = &s.cameras[0] 69 | } 70 | } 71 | 72 | func (s *Scene) CurrentCamera() *entity.Camera { 73 | return s.currentCamera 74 | } 75 | 76 | func (s *Scene) IsLoaded() bool { 77 | return s.isLoaded 78 | } 79 | 80 | // Empty the current scene 81 | func (s *Scene) Reset() { 82 | s.isLoaded = false 83 | 84 | s.currentCamera = nil 85 | s.cameras = make([]entity.Camera, 0) 86 | s.entities = make([]entity.IEntity, 0) 87 | s.world = &world.World{} 88 | } 89 | 90 | var currentScene Scene 91 | 92 | func Get() *Scene { 93 | return ¤tScene 94 | } 95 | -------------------------------------------------------------------------------- /scene/visibility/cache.go: -------------------------------------------------------------------------------- 1 | package visibility 2 | 3 | type Cache struct { 4 | Leafs []uint16 5 | ClusterId int16 6 | SkyVisible bool 7 | Faces []uint16 8 | } 9 | -------------------------------------------------------------------------------- /scene/visibility/vis.go: -------------------------------------------------------------------------------- 1 | package visibility 2 | 3 | import ( 4 | "github.com/galaco/bsp" 5 | "github.com/galaco/bsp/lumps" 6 | "github.com/galaco/bsp/primitives/leaf" 7 | "github.com/galaco/bsp/primitives/node" 8 | "github.com/galaco/bsp/primitives/plane" 9 | "github.com/galaco/bsp/primitives/visibility" 10 | "github.com/go-gl/mathgl/mgl32" 11 | ) 12 | 13 | type Vis struct { 14 | ClusterCache []Cache 15 | VisibilityLump *visibility.Vis 16 | Leafs []leaf.Leaf 17 | LeafFaces []uint16 18 | Nodes []node.Node 19 | Planes []plane.Plane 20 | 21 | viewPosition mgl32.Vec3 22 | viewCurrentLeaf *leaf.Leaf 23 | } 24 | 25 | func (vis *Vis) PVSForCluster(clusterId int16) []int16 { 26 | return vis.VisibilityLump.GetVisibleClusters(clusterId) 27 | } 28 | 29 | func (vis *Vis) GetPVSCacheForCluster(clusterId int16) *Cache { 30 | if clusterId == -1 { 31 | clusterId = int16(vis.findCurrentLeafIndex(vis.viewPosition)) 32 | } 33 | for _, cacheEntry := range vis.ClusterCache { 34 | if cacheEntry.ClusterId == clusterId { 35 | return &cacheEntry 36 | } 37 | } 38 | return vis.cachePVSForCluster(clusterId) 39 | } 40 | 41 | // Cache visible data for current cluster 42 | func (vis *Vis) cachePVSForCluster(clusterId int16) *Cache { 43 | clusterList := vis.VisibilityLump.GetPVSForCluster(clusterId) 44 | 45 | skyVisible := false 46 | 47 | faces := make([]uint16, 0) 48 | leafs := make([]uint16, 0) 49 | for idx, l := range vis.Leafs { 50 | //Check if cluster is in pvs 51 | if !vis.clusterVisible(&clusterList, l.Cluster) { 52 | continue 53 | } 54 | if l.Flags()&leaf.LeafFlagsSky > 0 { 55 | skyVisible = true 56 | } 57 | leafs = append(leafs, uint16(idx)) 58 | faces = append(faces, vis.LeafFaces[l.FirstLeafFace:l.FirstLeafFace+l.NumLeafFaces]...) 59 | } 60 | 61 | cache := Cache{ 62 | ClusterId: clusterId, 63 | Faces: faces, 64 | Leafs: leafs, 65 | SkyVisible: skyVisible, 66 | } 67 | 68 | vis.ClusterCache = append(vis.ClusterCache, cache) 69 | 70 | return &cache 71 | } 72 | 73 | // Determine if a cluster is visible 74 | func (vis *Vis) clusterVisible(pvs *[]bool, leafCluster int16) bool { 75 | if leafCluster < 0 { 76 | return true 77 | } 78 | 79 | if (*pvs)[leafCluster] { 80 | return true 81 | } 82 | 83 | return false 84 | } 85 | 86 | // Test if the camera has moved, and find the current leaf if so 87 | func (vis *Vis) FindCurrentLeaf(position mgl32.Vec3) *leaf.Leaf { 88 | if !vis.viewPosition.ApproxEqualThreshold(position, 0.000000001) { 89 | vis.viewPosition = position 90 | vis.viewCurrentLeaf = &vis.Leafs[vis.findCurrentLeafIndex(vis.viewPosition)] 91 | } 92 | return vis.viewCurrentLeaf 93 | } 94 | 95 | // Find the index into the leaf array for the leaf the player 96 | // is inside of 97 | // Based on: https://bitbucket.org/fallahn/chuf-arc 98 | func (vis *Vis) findCurrentLeafIndex(position mgl32.Vec3) int32 { 99 | i := int32(0) 100 | 101 | //walk the bsp to find the index of the leaf which contains our position 102 | for i >= 0 { 103 | node := &vis.Nodes[i] 104 | plane := vis.Planes[node.PlaneNum] 105 | 106 | //check which side of the plane the position is on so we know which direction to go 107 | distance := plane.Normal.X()*position.X() + plane.Normal.Y()*position.Y() + plane.Normal.Z()*position.Z() - plane.Distance 108 | i = node.Children[0] 109 | if distance < 0 { 110 | i = node.Children[1] 111 | } 112 | } 113 | 114 | return ^i 115 | } 116 | 117 | func NewVisFromBSP(file *bsp.Bsp) *Vis { 118 | return &Vis{ 119 | VisibilityLump: file.Lump(bsp.LumpVisibility).(*lumps.Visibility).GetData(), 120 | viewPosition: mgl32.Vec3{65536, 65536, 65536}, 121 | Leafs: file.Lump(bsp.LumpLeafs).(*lumps.Leaf).GetData(), 122 | LeafFaces: file.Lump(bsp.LumpLeafFaces).(*lumps.LeafFace).GetData(), 123 | Nodes: file.Lump(bsp.LumpNodes).(*lumps.Node).GetData(), 124 | Planes: file.Lump(bsp.LumpPlanes).(*lumps.Planes).GetData(), 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /scene/world/sky.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "github.com/galaco/lambda-core/entity" 5 | "github.com/galaco/lambda-core/model" 6 | "github.com/go-gl/mathgl/mgl32" 7 | ) 8 | 9 | type Sky struct { 10 | geometry *model.Bsp 11 | clusterLeafs []*model.ClusterLeaf 12 | transform entity.Transform 13 | 14 | cubemap *model.Model 15 | } 16 | 17 | func (sky *Sky) GetVisibleBsp() *model.Bsp { 18 | return sky.geometry 19 | } 20 | 21 | func (sky *Sky) GetClusterLeafs() []*model.ClusterLeaf { 22 | return sky.clusterLeafs 23 | } 24 | 25 | func (sky *Sky) GetCubemap() *model.Model { 26 | return sky.cubemap 27 | } 28 | 29 | func (sky *Sky) Transform() *entity.Transform { 30 | return &sky.transform 31 | } 32 | 33 | func NewSky(bsp *model.Bsp, clusterLeafs []*model.ClusterLeaf, position mgl32.Vec3, scale float32, skyCube *model.Model) *Sky { 34 | s := Sky{ 35 | geometry: bsp, 36 | clusterLeafs: clusterLeafs, 37 | cubemap: skyCube, 38 | } 39 | 40 | skyCameraPosition := (mgl32.Vec3{0, 0, 0}).Sub(position) 41 | skyCameraScale := mgl32.Vec3{scale, scale, scale} 42 | 43 | s.transform.Position = skyCameraPosition.Mul(scale) 44 | s.transform.Scale = skyCameraScale 45 | 46 | // remap prop transform to real world 47 | for _, l := range s.clusterLeafs { 48 | for _, prop := range l.StaticProps { 49 | prop.Transform().Position = prop.Transform().Position.Add(skyCameraPosition) 50 | prop.Transform().Position = prop.Transform().Position.Mul(scale) 51 | prop.Transform().Scale = skyCameraScale 52 | } 53 | } 54 | return &s 55 | } 56 | -------------------------------------------------------------------------------- /scene/world/world.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "github.com/galaco/bsp/primitives/leaf" 5 | "github.com/galaco/lambda-client/scene/visibility" 6 | "github.com/galaco/lambda-core/entity" 7 | "github.com/galaco/lambda-core/mesh" 8 | "github.com/galaco/lambda-core/model" 9 | "github.com/go-gl/mathgl/mgl32" 10 | "sync" 11 | ) 12 | 13 | type World struct { 14 | entity.Base 15 | staticModel model.Bsp 16 | 17 | staticProps []model.StaticProp 18 | sky Sky 19 | 20 | visibleClusterLeafs []*model.ClusterLeaf 21 | 22 | visData *visibility.Vis 23 | LeafCache *visibility.Cache 24 | currentLeaf *leaf.Leaf 25 | 26 | rebuildMutex sync.Mutex 27 | } 28 | 29 | func (entity *World) Bsp() *model.Bsp { 30 | return &entity.staticModel 31 | } 32 | 33 | func (entity *World) Sky() *Sky { 34 | return &entity.sky 35 | } 36 | 37 | func (entity *World) VisibleClusters() []*model.ClusterLeaf { 38 | entity.rebuildMutex.Lock() 39 | vw := entity.visibleClusterLeafs 40 | entity.rebuildMutex.Unlock() 41 | return vw 42 | } 43 | 44 | // Rebuild the current facelist to render, by first 45 | // recalculating using vvis data 46 | func (entity *World) TestVisibility(position mgl32.Vec3) { 47 | // View hasn't moved 48 | currentLeaf := entity.visData.FindCurrentLeaf(position) 49 | 50 | if currentLeaf == entity.currentLeaf { 51 | return 52 | } 53 | 54 | if currentLeaf == nil || currentLeaf.Cluster == -1 { 55 | // Still outside the world 56 | if entity.currentLeaf == nil { 57 | return 58 | } 59 | 60 | entity.currentLeaf = currentLeaf 61 | 62 | entity.AsyncRebuildVisibleWorld() 63 | return 64 | } 65 | 66 | // Haven't changed cluster 67 | if entity.LeafCache != nil && entity.LeafCache.ClusterId == currentLeaf.Cluster { 68 | return 69 | } 70 | 71 | entity.currentLeaf = currentLeaf 72 | entity.LeafCache = entity.visData.GetPVSCacheForCluster(currentLeaf.Cluster) 73 | 74 | entity.AsyncRebuildVisibleWorld() 75 | } 76 | 77 | // Launches rebuilding the visible world in a separate thread 78 | // Note: This *could* cause rendering issues if the rebuild is slower than 79 | // travelling between clusters 80 | func (entity *World) AsyncRebuildVisibleWorld() { 81 | func(currentLeaf *leaf.Leaf) { 82 | visibleWorld := make([]*model.ClusterLeaf, 0) 83 | 84 | visibleClusterIds := make([]int16, 0) 85 | 86 | if currentLeaf != nil && currentLeaf.Cluster != -1 { 87 | visibleClusterIds = entity.visData.PVSForCluster(currentLeaf.Cluster) 88 | } 89 | 90 | // nothing visible so render everything 91 | if len(visibleClusterIds) == 0 { 92 | for idx := range entity.staticModel.ClusterLeafs() { 93 | visibleWorld = append(visibleWorld, &entity.staticModel.ClusterLeafs()[idx]) 94 | } 95 | } else { 96 | for _, clusterId := range visibleClusterIds { 97 | visibleWorld = append(visibleWorld, &entity.staticModel.ClusterLeafs()[clusterId]) 98 | } 99 | } 100 | 101 | entity.rebuildMutex.Lock() 102 | entity.visibleClusterLeafs = visibleWorld 103 | entity.rebuildMutex.Unlock() 104 | }(entity.currentLeaf) 105 | } 106 | 107 | // Build skybox from tree 108 | func (entity *World) BuildSkybox(sky *model.Model, position mgl32.Vec3, scale float32) { 109 | // Rebuild bsp faces 110 | visibleModel := model.NewBsp(entity.staticModel.Mesh().(*mesh.Mesh)) 111 | 112 | visibleWorld := make([]*model.ClusterLeaf, 0) 113 | 114 | l := entity.visData.FindCurrentLeaf(position) 115 | visibleClusterIds := entity.visData.PVSForCluster(l.Cluster) 116 | 117 | // nothing visible so render everything 118 | if len(visibleClusterIds) == 0 { 119 | for idx := range entity.staticModel.ClusterLeafs() { 120 | visibleWorld = append(visibleWorld, &entity.staticModel.ClusterLeafs()[idx]) 121 | } 122 | } else { 123 | for clusterId := range visibleClusterIds { 124 | visibleWorld = append(visibleWorld, &entity.staticModel.ClusterLeafs()[clusterId]) 125 | } 126 | } 127 | 128 | entity.sky = *NewSky(visibleModel, visibleWorld, position, scale, sky) 129 | } 130 | 131 | func NewWorld(world model.Bsp, staticProps []model.StaticProp, visData *visibility.Vis) *World { 132 | c := World{ 133 | staticModel: world, 134 | staticProps: staticProps, 135 | visData: visData, 136 | } 137 | 138 | c.TestVisibility(mgl32.Vec3{0, 0, 0}) 139 | 140 | return &c 141 | } 142 | -------------------------------------------------------------------------------- /ui/dialogs/syscalls.go: -------------------------------------------------------------------------------- 1 | package dialogs 2 | 3 | import ( 4 | "github.com/sqweek/dialog" 5 | ) 6 | 7 | // OpenFile 8 | func OpenFile(filterDescription, extension string) (string, error) { 9 | return dialog.File().Filter(filterDescription, extension).Load() 10 | } 11 | 12 | // ErrorMessage 13 | func ErrorMessage(err error) { 14 | dialog.Message("%s", err.Error()).Error() 15 | } 16 | -------------------------------------------------------------------------------- /ui/ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/engine" 5 | vguiCore "github.com/galaco/lambda-core/loader/vgui" 6 | "github.com/galaco/lambda-core/vgui" 7 | "github.com/galaco/tinygametools" 8 | "github.com/galaco/filesystem" 9 | ) 10 | 11 | type Gui struct { 12 | engine.Manager 13 | window *tinygametools.Window 14 | masterPanel vgui.MasterPanel 15 | } 16 | 17 | func (ui *Gui) Register() { 18 | 19 | } 20 | 21 | func (ui *Gui) Update(dt float64) { 22 | ui.Render() 23 | } 24 | 25 | func (ui *Gui) Render() { 26 | ui.masterPanel.Draw() 27 | } 28 | 29 | // LoadVGUIResource 30 | func (ui *Gui) LoadVGUIResource(fs *filesystem.FileSystem, filename string) error { 31 | p, err := vguiCore.LoadVGUI(fs, filename) 32 | if err != nil { 33 | return err 34 | } 35 | ui.masterPanel.AddChild(p) 36 | 37 | return nil 38 | } 39 | 40 | func (ui *Gui) MasterPanel() *vgui.MasterPanel { 41 | return &ui.masterPanel 42 | } 43 | 44 | func NewGUIManager(win *tinygametools.Window) *Gui { 45 | return &Gui{ 46 | window: win, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /window/manager.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "github.com/galaco/lambda-client/engine" 5 | "github.com/galaco/tinygametools" 6 | "github.com/go-gl/glfw/v3.2/glfw" 7 | ) 8 | 9 | // Manager is responsible for managing this games window. Understand 10 | // that there is a distinction between the window and the renderer. 11 | // This manager provides a window that a rendering context can be 12 | // obtained from, and device input handling. 13 | type Manager struct { 14 | engine.Manager 15 | window *tinygametools.Window 16 | 17 | Name string 18 | } 19 | 20 | // Register will create a new Window 21 | func (manager *Manager) Register() { 22 | } 23 | 24 | // Update simply calls the input manager that uses this window 25 | func (manager *Manager) Update(dt float64) { 26 | } 27 | 28 | // Unregister will end input listening and kill any window 29 | func (manager *Manager) Unregister() { 30 | glfw.Terminate() 31 | } 32 | 33 | // PostUpdate is called at the end of an update loop. 34 | // In this case it simply SwapBuffers the window, (to display updated window 35 | // contents) 36 | func (manager *Manager) PostUpdate() { 37 | manager.window.Handle().SwapBuffers() 38 | } 39 | 40 | // NewWindowManager 41 | func NewWindowManager(win *tinygametools.Window) *Manager { 42 | return &Manager{ 43 | window: win, 44 | } 45 | } 46 | --------------------------------------------------------------------------------