├── .github └── workflows │ └── scala.yml ├── .gitignore ├── .scalafmt.conf ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src └── main ├── resources ├── fonts │ ├── OpenSans-Bold.ttf │ └── OpenSans-Regular.ttf ├── models │ ├── bob │ │ ├── boblamp.md5anim │ │ ├── boblamp.md5mesh │ │ ├── guard1_body.png │ │ ├── guard1_body_h.png │ │ ├── guard1_body_s.png │ │ ├── guard1_face.png │ │ ├── guard1_face_local.png │ │ ├── guard1_face_s.png │ │ ├── guard1_helmet.png │ │ ├── guard1_helmet_h.png │ │ ├── guard1_helmet_s.png │ │ ├── iron_grill.png │ │ ├── round_grill.png │ │ └── round_grill_h.png │ ├── cybertruck │ │ ├── tesla_ct_export1123.fbx │ │ ├── tex1.png │ │ ├── tex1_DSP.png │ │ ├── texLOD1.png │ │ ├── texLOD1_NRM.png │ │ ├── texMain.png │ │ ├── texMain_DSP.png │ │ └── texMain_NRM.png │ ├── house │ │ ├── SOURCE.txt │ │ ├── house.mtl │ │ ├── house.obj │ │ ├── house_text.jpg │ │ └── house_text_bmap.jpg │ ├── monster │ │ ├── gob.png │ │ ├── gob2.png │ │ ├── gob2_s.png │ │ ├── gob_s.png │ │ ├── hellknight.png │ │ ├── hellknight_local.png │ │ ├── hellknight_s.png │ │ ├── monster.md5anim │ │ ├── monster.md5mesh │ │ ├── tongue.png │ │ ├── tongue_local.png │ │ └── tongue_s.png │ ├── particle │ │ ├── particle.obj │ │ ├── particle.png │ │ └── particle_anim.png │ ├── skybox │ │ ├── drakeq_bk.tga │ │ ├── drakeq_dn.tga │ │ ├── drakeq_ft.tga │ │ ├── drakeq_lf.tga │ │ ├── drakeq_rt.tga │ │ └── drakeq_up.tga │ └── tree │ │ ├── diffus.tga │ │ ├── normal.tga │ │ ├── palm_tree.FBX │ │ ├── palm_tree.obj │ │ ├── preview.jpg │ │ ├── readme.txt │ │ └── specular.tga └── textures │ ├── cubemap.jpg │ ├── grassblock.png │ ├── heightmap.png │ └── terrain.png └── scala └── zio3d ├── core ├── assimp │ └── package.scala ├── buffers │ └── package.scala ├── gl │ ├── package.scala │ └── types.scala ├── glfw │ ├── package.scala │ └── types.scala ├── images │ ├── Image.scala │ └── package.scala ├── math │ ├── AxisAngle4.scala │ ├── Matrix4.scala │ ├── Quaternion.scala │ ├── Vector2.scala │ ├── Vector3.scala │ ├── Vector4.scala │ └── package.scala ├── nvg │ ├── NVGContext.scala │ └── package.scala └── package.scala ├── engine ├── Box2D.scala ├── Camera.scala ├── Fire.scala ├── Fixtures.scala ├── Fog.scala ├── GameItem.scala ├── Gun.scala ├── HeightMapMesh.scala ├── LightSources.scala ├── Mesh.scala ├── MeshDefinition.scala ├── Particle.scala ├── Perspective.scala ├── SkyboxDefinition.scala ├── Terrain.scala ├── Transformation.scala ├── UserInput.scala ├── glwindow │ └── package.scala ├── loaders │ ├── LoadingError.scala │ ├── assimp │ │ ├── anim │ │ │ ├── package.scala │ │ │ └── types.scala │ │ └── static │ │ │ └── package.scala │ ├── heightmap │ │ └── package.scala │ ├── particles │ │ └── package.scala │ ├── terrain │ │ └── package.scala │ └── texture │ │ └── package.scala ├── package.scala ├── runtime │ └── MainThreadApp.scala └── shaders │ ├── ShaderInterpreter.scala │ ├── particle │ ├── ParticleShaderProgram.scala │ └── package.scala │ ├── scene │ ├── package.scala │ └── types.scala │ ├── simple │ ├── SimpleShaderProgram.scala │ └── package.scala │ └── skybox │ ├── SkyboxShaderProgram.scala │ └── package.scala └── game ├── Game.scala ├── GameResources.scala ├── Main.scala ├── Zio3dGame.scala ├── Zio3dState.scala ├── config └── GameConfig.scala ├── hud ├── package.scala └── types.scala └── package.scala /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 1.8 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 1.8 20 | - name: Run tests 21 | run: sbt test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | project/target/ 3 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.0.0" 2 | maxColumn = 120 3 | align = most 4 | continuationIndent.defnSite = 2 5 | assumeStandardLibraryStripMargin = true 6 | docstrings = JavaDoc 7 | lineEndings = preserve 8 | includeCurlyBraceInSelectChains = false 9 | danglingParentheses = true 10 | optIn.annotationNewlines = true 11 | 12 | rewrite.rules = [SortImports, RedundantBraces] 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.13.1 4 | jdk: 5 | - openjdk11 6 | 7 | script: sbt ++$TRAVIS_SCALA_VERSION compile 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZIO 3D 2 | 3 | [![Build Status](https://travis-ci.com/wongelz/zio3d.svg?branch=master)](https://travis-ci.com/wongelz/zio3d) 4 | 5 | An experimental 3D game built with [ZIO](https://zio.dev/) and OpenGL ([LWJGL](https://www.lwjgl.org/)). 6 | 7 | ## Quick start 8 | 9 | `sbt run` 10 | 11 | ## Sources & acknowledgements 12 | 13 | - https://ahbejarano.gitbook.io/lwjglgamedev/ and associated source code https://github.com/lwjglgamedev/lwjglbook 14 | - https://github.com/JOML-CI/JOML 15 | - http://www.custommapmakers.org/skyboxes.php 16 | - https://opengameart.org 17 | - [Tesla Cybertruck by hashikemu](https://sketchfab.com/3d-models/tesla-cybertruck-657e71b3e2ad468196668e9c9df708fb) licenced under [CC Attribution](https://creativecommons.org/licenses/by/4.0/) 18 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import scala.collection.immutable.Seq 2 | 3 | name := "zio3d" 4 | 5 | version := "0.1" 6 | 7 | scalaVersion := "2.13.0" 8 | 9 | lazy val zioVersion = "1.0.4" 10 | lazy val lwjglVersion = "3.2.1" 11 | 12 | lazy val os = Option(System.getProperty("os.name", "")) 13 | .map(_.substring(0, 3).toLowerCase) match { 14 | case Some("win") => "windows" 15 | case Some("mac") => "macos" 16 | case _ => "linux" 17 | } 18 | 19 | resolvers += Resolver.sonatypeRepo("snapshots") 20 | 21 | libraryDependencies ++= Seq( 22 | "dev.zio" %% "zio" % zioVersion, 23 | "dev.zio" %% "zio-streams" % zioVersion, 24 | "org.lwjgl" % "lwjgl" % lwjglVersion, 25 | "org.lwjgl" % "lwjgl-opengl" % lwjglVersion, 26 | "org.lwjgl" % "lwjgl-glfw" % lwjglVersion, 27 | "org.lwjgl" % "lwjgl-stb" % lwjglVersion, 28 | "org.lwjgl" % "lwjgl-assimp" % lwjglVersion, 29 | "org.lwjgl" % "lwjgl-nanovg" % lwjglVersion, 30 | "org.lwjgl" % "lwjgl" % lwjglVersion classifier s"natives-$os", 31 | "org.lwjgl" % "lwjgl-opengl" % lwjglVersion classifier s"natives-$os", 32 | "org.lwjgl" % "lwjgl-glfw" % lwjglVersion classifier s"natives-$os", 33 | "org.lwjgl" % "lwjgl-stb" % lwjglVersion classifier s"natives-$os", 34 | "org.lwjgl" % "lwjgl-assimp" % lwjglVersion classifier s"natives-$os", 35 | "org.lwjgl" % "lwjgl-nanovg" % lwjglVersion classifier s"natives-$os" 36 | ) 37 | 38 | scalacOptions ++= Seq( 39 | "-deprecation", 40 | "-encoding", "UTF-8", 41 | "-feature", 42 | "-language:existentials", 43 | "-language:higherKinds", 44 | "-language:implicitConversions", 45 | "-unchecked", 46 | "-Xfatal-warnings", 47 | "-Xlint:_,-missing-interpolator", 48 | "-Ywarn-dead-code", 49 | "-Ywarn-numeric-widen", 50 | "-Ywarn-value-discard", 51 | "-Yrangepos", 52 | "-target:jvm-1.8" 53 | ) 54 | 55 | javaOptions ++= { 56 | if (os == "macos") 57 | Seq("-XstartOnFirstThread") 58 | else 59 | Nil 60 | } 61 | 62 | fork in run := true 63 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0") -------------------------------------------------------------------------------- /src/main/resources/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/main/resources/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/main/resources/models/bob/guard1_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/guard1_body.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/guard1_body_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/guard1_body_h.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/guard1_body_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/guard1_body_s.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/guard1_face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/guard1_face.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/guard1_face_local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/guard1_face_local.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/guard1_face_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/guard1_face_s.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/guard1_helmet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/guard1_helmet.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/guard1_helmet_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/guard1_helmet_h.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/guard1_helmet_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/guard1_helmet_s.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/iron_grill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/iron_grill.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/round_grill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/round_grill.png -------------------------------------------------------------------------------- /src/main/resources/models/bob/round_grill_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/bob/round_grill_h.png -------------------------------------------------------------------------------- /src/main/resources/models/cybertruck/tesla_ct_export1123.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/cybertruck/tesla_ct_export1123.fbx -------------------------------------------------------------------------------- /src/main/resources/models/cybertruck/tex1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/cybertruck/tex1.png -------------------------------------------------------------------------------- /src/main/resources/models/cybertruck/tex1_DSP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/cybertruck/tex1_DSP.png -------------------------------------------------------------------------------- /src/main/resources/models/cybertruck/texLOD1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/cybertruck/texLOD1.png -------------------------------------------------------------------------------- /src/main/resources/models/cybertruck/texLOD1_NRM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/cybertruck/texLOD1_NRM.png -------------------------------------------------------------------------------- /src/main/resources/models/cybertruck/texMain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/cybertruck/texMain.png -------------------------------------------------------------------------------- /src/main/resources/models/cybertruck/texMain_DSP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/cybertruck/texMain_DSP.png -------------------------------------------------------------------------------- /src/main/resources/models/cybertruck/texMain_NRM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/cybertruck/texMain_NRM.png -------------------------------------------------------------------------------- /src/main/resources/models/house/SOURCE.txt: -------------------------------------------------------------------------------- 1 | https://free3d.com/3d-model/old-farm-house-91130.html -------------------------------------------------------------------------------- /src/main/resources/models/house/house.mtl: -------------------------------------------------------------------------------- 1 | newmtl lambert13SG 2 | illum 4 3 | Kd 0.00 0.00 0.00 4 | Ka 0.00 0.00 0.00 5 | Tf 1.00 1.00 1.00 6 | map_Kd house_text.jpg 7 | bump house_text_bmap.jpg -bm 0.012 8 | Ni 1.00 9 | -------------------------------------------------------------------------------- /src/main/resources/models/house/house_text.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/house/house_text.jpg -------------------------------------------------------------------------------- /src/main/resources/models/house/house_text_bmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/house/house_text_bmap.jpg -------------------------------------------------------------------------------- /src/main/resources/models/monster/gob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/gob.png -------------------------------------------------------------------------------- /src/main/resources/models/monster/gob2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/gob2.png -------------------------------------------------------------------------------- /src/main/resources/models/monster/gob2_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/gob2_s.png -------------------------------------------------------------------------------- /src/main/resources/models/monster/gob_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/gob_s.png -------------------------------------------------------------------------------- /src/main/resources/models/monster/hellknight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/hellknight.png -------------------------------------------------------------------------------- /src/main/resources/models/monster/hellknight_local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/hellknight_local.png -------------------------------------------------------------------------------- /src/main/resources/models/monster/hellknight_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/hellknight_s.png -------------------------------------------------------------------------------- /src/main/resources/models/monster/tongue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/tongue.png -------------------------------------------------------------------------------- /src/main/resources/models/monster/tongue_local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/tongue_local.png -------------------------------------------------------------------------------- /src/main/resources/models/monster/tongue_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/monster/tongue_s.png -------------------------------------------------------------------------------- /src/main/resources/models/particle/particle.obj: -------------------------------------------------------------------------------- 1 | v -1.000000 1.000000 0.000000 2 | v -1.000000 -1.000000 0.000000 3 | v 1.000000 -1.000000 0.000000 4 | v 1.000000 1.000000 0.000000 5 | 6 | vt 0.000000 0.000000 7 | vt 0.000000 1.000000 8 | vt 1.000000 1.000000 9 | vt 1.000000 0.000000 10 | 11 | vn 0.000000 0.000000 1.000000 12 | 13 | f 1/1/1 2/2/1 3/3/1 14 | f 1/1/1 3/3/1 4/4/1 -------------------------------------------------------------------------------- /src/main/resources/models/particle/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/particle/particle.png -------------------------------------------------------------------------------- /src/main/resources/models/particle/particle_anim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/particle/particle_anim.png -------------------------------------------------------------------------------- /src/main/resources/models/skybox/drakeq_bk.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/skybox/drakeq_bk.tga -------------------------------------------------------------------------------- /src/main/resources/models/skybox/drakeq_dn.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/skybox/drakeq_dn.tga -------------------------------------------------------------------------------- /src/main/resources/models/skybox/drakeq_ft.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/skybox/drakeq_ft.tga -------------------------------------------------------------------------------- /src/main/resources/models/skybox/drakeq_lf.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/skybox/drakeq_lf.tga -------------------------------------------------------------------------------- /src/main/resources/models/skybox/drakeq_rt.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/skybox/drakeq_rt.tga -------------------------------------------------------------------------------- /src/main/resources/models/skybox/drakeq_up.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/skybox/drakeq_up.tga -------------------------------------------------------------------------------- /src/main/resources/models/tree/diffus.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/tree/diffus.tga -------------------------------------------------------------------------------- /src/main/resources/models/tree/normal.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/tree/normal.tga -------------------------------------------------------------------------------- /src/main/resources/models/tree/palm_tree.FBX: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/tree/palm_tree.FBX -------------------------------------------------------------------------------- /src/main/resources/models/tree/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/models/tree/preview.jpg -------------------------------------------------------------------------------- /src/main/resources/models/tree/readme.txt: -------------------------------------------------------------------------------- 1 | Provided by Nobiax. 2 | 3 | Absolutely free to use or to modify in any kind of work (personal, commercial or else). 4 | 5 | Give me a link of the result at nobiax.deviantart.com or my other account on OpenGameArt.com or ShareCG.com 6 | ( watch my journal on deviantart ) ;) 7 | -------------------------------------------------------------------------------- /src/main/resources/textures/cubemap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/textures/cubemap.jpg -------------------------------------------------------------------------------- /src/main/resources/textures/grassblock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/textures/grassblock.png -------------------------------------------------------------------------------- /src/main/resources/textures/heightmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/textures/heightmap.png -------------------------------------------------------------------------------- /src/main/resources/textures/terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wongelz/zio3d/41dcdd66c80df7a2ef7f22fba2b967b9698d5f98/src/main/resources/textures/terrain.png -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/assimp/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core 2 | 3 | import java.nio.IntBuffer 4 | import java.nio.file.Path 5 | 6 | import org.lwjgl.assimp.Assimp.{aiGetErrorString, aiGetMaterialColor, aiGetMaterialTexture, aiImportFile} 7 | import org.lwjgl.assimp.{AIColor4D, AIMaterial, AIScene, AIString} 8 | import zio._ 9 | import zio3d.core.math.Vector4 10 | import zio3d.engine.MaterialDefinition 11 | import zio3d.engine.loaders.LoadingError 12 | 13 | package object assimp { 14 | 15 | type Assimp = Has[Assimp.Service] 16 | 17 | object Assimp extends Serializable { 18 | trait Service { 19 | def importFile(resourcePath: Path, flags: Int): IO[LoadingError, AIScene] 20 | 21 | def getMaterialTexture(mat: AIMaterial, `type`: Int): UIO[Option[String]] 22 | 23 | def getMaterialColor(mat: AIMaterial, pKey: String, `type`: Int): UIO[Vector4] 24 | } 25 | 26 | val live = ZLayer.succeed[Assimp.Service] { 27 | new Service { 28 | def getErrorString: UIO[Option[String]] = 29 | UIO.effectTotal { Option(aiGetErrorString()) } 30 | 31 | override def importFile(resourcePath: Path, flags: Int) = 32 | IO.fromOption(Option(aiImportFile(resourcePath.toString, flags))).flatMapError { _ => 33 | getErrorString.map { str => 34 | LoadingError.FileLoadError(resourcePath.toString, str.getOrElse("No error reported by Assimp")) 35 | } 36 | } 37 | 38 | override def getMaterialTexture(mat: AIMaterial, `type`: Int): UIO[Option[String]] = 39 | UIO.effectTotal { 40 | val path = AIString.calloc() 41 | aiGetMaterialTexture(mat, `type`, 0, path, null.asInstanceOf[IntBuffer], null, null, null, null, null) 42 | if (path.length() == 0) None else Some(path.dataString()) 43 | } 44 | 45 | override def getMaterialColor(mat: AIMaterial, pKey: String, `type`: Int) = 46 | UIO.effectTotal { 47 | val color = AIColor4D.create() 48 | val result = aiGetMaterialColor(mat, pKey, `type`, 0, color) 49 | if (result == 0) { 50 | Vector4(color.r(), color.g(), color.b(), color.a()) 51 | } else { 52 | MaterialDefinition.DefaultColour 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | final val assimp: ZIO[Assimp, Nothing, Assimp.Service] = 60 | ZIO.access(_.get) 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/buffers/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core 2 | 3 | import java.nio.{ByteBuffer, DoubleBuffer, FloatBuffer, IntBuffer} 4 | 5 | import org.lwjgl.BufferUtils 6 | import zio._ 7 | 8 | package object buffers { 9 | 10 | type Buffers = Has[Buffers.Service] 11 | 12 | object Buffers extends Serializable { 13 | 14 | trait Service { 15 | def intBuffer(capacity: Int): UIO[IntBuffer] 16 | 17 | def floatBuffer(capacity: Int): UIO[FloatBuffer] 18 | 19 | def doubleBuffer(capacity: Int): UIO[DoubleBuffer] 20 | 21 | def byteBuffer(capacity: Int): UIO[ByteBuffer] 22 | 23 | def intBuffer(data: Array[Int]): UIO[IntBuffer] 24 | 25 | def floatBuffer(data: Array[Float]): UIO[FloatBuffer] 26 | 27 | def byteBuffer(data: Array[Byte]): UIO[ByteBuffer] 28 | } 29 | 30 | val live = ZLayer.succeed { 31 | new Service { 32 | def intBuffer(capacity: Int) = 33 | IO.effectTotal { 34 | BufferUtils.createIntBuffer(capacity) 35 | } 36 | 37 | def floatBuffer(capacity: Int) = 38 | IO.effectTotal { 39 | BufferUtils.createFloatBuffer(capacity) 40 | } 41 | 42 | def doubleBuffer(capacity: Int) = 43 | IO.effectTotal { 44 | BufferUtils.createDoubleBuffer(capacity) 45 | } 46 | 47 | def byteBuffer(capacity: Int) = 48 | IO.effectTotal { 49 | BufferUtils.createByteBuffer(capacity) 50 | } 51 | 52 | def intBuffer(data: Array[Int]) = 53 | IO.effectTotal { 54 | BufferUtils 55 | .createIntBuffer(data.length) 56 | .put(data) 57 | .flip() 58 | .asInstanceOf[IntBuffer] 59 | } 60 | 61 | def floatBuffer(data: Array[Float]) = 62 | IO.effectTotal { 63 | BufferUtils 64 | .createFloatBuffer(data.length) 65 | .put(data) 66 | .flip() 67 | .asInstanceOf[FloatBuffer] 68 | } 69 | 70 | def byteBuffer(data: Array[Byte]) = 71 | IO.effectTotal { 72 | BufferUtils 73 | .createByteBuffer(data.length) 74 | .put(data) 75 | .flip() 76 | .asInstanceOf[ByteBuffer] 77 | } 78 | } 79 | } 80 | } 81 | 82 | final val buffers: ZIO[Buffers, Nothing, Buffers.Service] = 83 | ZIO.access(_.get) 84 | 85 | final def intBuffer(capacity: Int): ZIO[Buffers, Nothing, IntBuffer] = 86 | ZIO.accessM(_.get.intBuffer(capacity)) 87 | 88 | def floatBuffer(capacity: Int): ZIO[Buffers, Nothing, FloatBuffer] = 89 | ZIO.accessM(_.get.floatBuffer(capacity)) 90 | 91 | final def doubleBuffer(capacity: Int): ZIO[Buffers, Nothing, DoubleBuffer] = 92 | ZIO.accessM(_.get.doubleBuffer(capacity)) 93 | 94 | final def byteBuffer(capacity: Int): ZIO[Buffers, Nothing, ByteBuffer] = 95 | ZIO.accessM(_.get.byteBuffer(capacity)) 96 | 97 | final def intBuffer(data: Array[Int]): ZIO[Buffers, Nothing, IntBuffer] = 98 | ZIO.accessM(_.get.intBuffer(data)) 99 | 100 | final def floatBuffer(data: Array[Float]): ZIO[Buffers, Nothing, FloatBuffer] = 101 | ZIO.accessM(_.get.floatBuffer(data)) 102 | 103 | final def byteBuffer(data: Array[Byte]): ZIO[Buffers, Nothing, ByteBuffer] = 104 | ZIO.accessM(_.get.byteBuffer(data)) 105 | } 106 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/gl/types.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.gl 2 | 3 | final case class Shader(ref: Int) extends AnyVal 4 | 5 | final case class Program(ref: Int) extends AnyVal 6 | 7 | final case class VertexArrayObject(ref: Int) extends AnyVal 8 | 9 | object VertexArrayObject { 10 | final val None = VertexArrayObject(0) 11 | } 12 | 13 | final case class VertexBufferObject(ref: Int) extends AnyVal 14 | 15 | object VertexBufferObject { 16 | final val None = VertexBufferObject(0) 17 | } 18 | 19 | final case class UniformLocation(loc: Int) extends AnyVal 20 | 21 | final case class AttribLocation(loc: Int) extends AnyVal 22 | 23 | object Program { 24 | final val None = Program(0) 25 | } 26 | 27 | final case class Texture(value: Int) extends AnyVal 28 | 29 | object Texture { 30 | final val None = Texture(0) 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/glfw/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core 2 | 3 | import java.io.PrintStream 4 | 5 | import org.lwjgl.glfw.Callbacks.glfwFreeCallbacks 6 | import org.lwjgl.glfw.GLFW._ 7 | import org.lwjgl.glfw.{GLFWErrorCallback, GLFWKeyCallback} 8 | import zio._ 9 | import zio3d.core.buffers.Buffers 10 | 11 | package object glfw { 12 | 13 | type GLFW = Has[GLFW.Service] 14 | 15 | object GLFW extends Serializable { 16 | 17 | trait Service { 18 | def setErrorPrintStream(printStream: PrintStream): UIO[Unit] 19 | 20 | def setWindowTitle(window: Window, title: String): UIO[Unit] 21 | 22 | def init: UIO[Boolean] 23 | 24 | def defaultWindowHints: UIO[Unit] 25 | 26 | def windowHint(hint: Int, value: Int): UIO[Unit] 27 | 28 | def createWindow(width: Int, height: Int, title: String, monitor: Long, share: Long): UIO[Window] 29 | 30 | def maximizeWindow(window: Window): UIO[Unit] 31 | 32 | def setKeyCallback(window: Window, callback: (Window, Int, Int, Int, Int) => Unit): UIO[GLFWKeyCallback] 33 | 34 | def freeCallbacks(window: Window): UIO[Unit] 35 | 36 | def destroyWindow(window: Window): UIO[Unit] 37 | 38 | def terminate: UIO[Unit] 39 | 40 | def setWindowShouldClose(window: Window, value: Boolean): UIO[Unit] 41 | 42 | def windowShouldClose(window: Window): UIO[Boolean] 43 | 44 | def makeContextCurrent(window: Window): UIO[Unit] 45 | 46 | def swapInterval(interval: Int): UIO[Unit] 47 | 48 | def showWindow(window: Window): UIO[Unit] 49 | 50 | def focusWindow(window: Window): UIO[Unit] 51 | 52 | def swapBuffers(window: Window): UIO[Unit] 53 | 54 | def pollEvents: UIO[Unit] 55 | 56 | def getKey(window: Window, key: Int): UIO[Int] 57 | 58 | def getMouseButton(window: Window, button: Int): UIO[Int] 59 | 60 | def setInputMode(window: Window, mode: Int, value: Int): UIO[Unit] 61 | 62 | def getCursorPos(window: Window): UIO[CursorPos] 63 | 64 | def setCursorPos(window: Window, xpos: Double, ypos: Double): UIO[Unit] 65 | 66 | def createStandardCursor(window: Window, shape: Int): UIO[Cursor] 67 | 68 | def setCursor(window: Window, cursor: Cursor): UIO[Unit] 69 | 70 | def getWindowSize(window: Window): UIO[WindowSize] 71 | } 72 | 73 | val live = ZLayer.fromService[Buffers.Service, GLFW.Service] { buffers => 74 | new Service { 75 | 76 | def setErrorPrintStream(printStream: PrintStream): UIO[Unit] = 77 | IO.effectTotal { 78 | GLFWErrorCallback.createPrint(printStream).set; 79 | () 80 | } 81 | 82 | def setWindowTitle(window: Window, title: String): UIO[Unit] = 83 | IO.effectTotal { 84 | glfwSetWindowTitle(window.value, title) 85 | } 86 | 87 | def init: UIO[Boolean] = 88 | IO.effectTotal { 89 | glfwInit() 90 | } 91 | 92 | def defaultWindowHints: UIO[Unit] = 93 | IO.effectTotal { 94 | glfwDefaultWindowHints() 95 | } 96 | 97 | def windowHint(hint: Int, value: Int): UIO[Unit] = 98 | IO.effectTotal { 99 | glfwWindowHint(hint, value) 100 | } 101 | 102 | def createWindow(width: Int, height: Int, title: String, monitor: Long, share: Long): UIO[Window] = 103 | IO.effectTotal { 104 | Window(glfwCreateWindow(width, height, title, monitor, share)) 105 | } 106 | 107 | def maximizeWindow(window: Window): UIO[Unit] = 108 | IO.effectTotal { 109 | glfwMaximizeWindow(window.value) 110 | } 111 | 112 | def setKeyCallback(window: Window, callback: (Window, Int, Int, Int, Int) => Unit): UIO[GLFWKeyCallback] = 113 | IO.effectTotal { 114 | glfwSetKeyCallback( 115 | window.value, 116 | (window: Long, key: Int, scancode: Int, action: Int, mods: Int) => 117 | callback(Window(window), key, scancode, action, mods) 118 | ) 119 | } 120 | 121 | def freeCallbacks(window: Window): UIO[Unit] = 122 | IO.effectTotal { 123 | glfwFreeCallbacks(window.value) 124 | } 125 | 126 | def destroyWindow(window: Window): UIO[Unit] = 127 | IO.effectTotal { 128 | glfwDestroyWindow(window.value) 129 | } 130 | 131 | def terminate: UIO[Unit] = 132 | IO.effectTotal { 133 | glfwTerminate() 134 | } 135 | 136 | def setWindowShouldClose(window: Window, value: Boolean): UIO[Unit] = 137 | IO.effectTotal { 138 | glfwSetWindowShouldClose(window.value, value) 139 | } 140 | 141 | def windowShouldClose(window: Window): UIO[Boolean] = 142 | IO.effectTotal { 143 | glfwWindowShouldClose(window.value) 144 | } 145 | 146 | def makeContextCurrent(window: Window): UIO[Unit] = 147 | IO.effectTotal { 148 | glfwMakeContextCurrent(window.value) 149 | } 150 | 151 | def swapInterval(interval: Int): UIO[Unit] = 152 | IO.effectTotal { 153 | glfwSwapInterval(interval) 154 | } 155 | 156 | def showWindow(window: Window): UIO[Unit] = 157 | IO.effectTotal { 158 | glfwShowWindow(window.value) 159 | } 160 | 161 | def focusWindow(window: Window): UIO[Unit] = 162 | IO.effectTotal { 163 | glfwFocusWindow(window.value) 164 | } 165 | 166 | def swapBuffers(window: Window): UIO[Unit] = 167 | IO.effectTotal { 168 | glfwSwapBuffers(window.value) 169 | } 170 | 171 | def pollEvents: UIO[Unit] = 172 | IO.effectTotal { 173 | glfwPollEvents() 174 | } 175 | 176 | def getKey(window: Window, key: Int): UIO[Int] = 177 | IO.effectTotal { 178 | glfwGetKey(window.value, key) 179 | } 180 | 181 | def getMouseButton(window: Window, button: Int): UIO[Int] = 182 | IO.effectTotal { 183 | glfwGetMouseButton(window.value, button) 184 | } 185 | 186 | def setInputMode(window: Window, mode: Int, value: Int): UIO[Unit] = 187 | IO.effectTotal { 188 | glfwSetInputMode(window.value, mode, value) 189 | } 190 | 191 | def getCursorPos(window: Window): UIO[CursorPos] = 192 | for { 193 | x <- buffers.doubleBuffer(1) 194 | y <- buffers.doubleBuffer(1) 195 | _ <- IO.effectTotal { 196 | glfwGetCursorPos(window.value, x, y) 197 | } 198 | } yield CursorPos(x.get(), y.get()) 199 | 200 | def setCursorPos(window: Window, xpos: Double, ypos: Double): UIO[Unit] = 201 | IO.effectTotal { 202 | glfwSetCursorPos(window.value, xpos, ypos) 203 | } 204 | 205 | def createStandardCursor(window: Window, shape: Int): UIO[Cursor] = 206 | IO.effectTotal { 207 | Cursor(glfwCreateStandardCursor(shape)) 208 | } 209 | 210 | def setCursor(window: Window, cursor: Cursor): UIO[Unit] = 211 | IO.effectTotal { 212 | glfwSetCursor(window.value, cursor.value) 213 | } 214 | 215 | def getWindowSize(window: Window): UIO[WindowSize] = 216 | for { 217 | w <- buffers.intBuffer(1) 218 | h <- buffers.intBuffer(1) 219 | _ <- IO.effectTotal { 220 | glfwGetWindowSize(window.value, w, h) 221 | } 222 | } yield WindowSize(w.get(), h.get()) 223 | } 224 | } 225 | } 226 | 227 | final val glfwService: ZIO[GLFW, Nothing, GLFW.Service] = 228 | ZIO.access(_.get) 229 | 230 | final def setErrorPrintStream(printStream: PrintStream): ZIO[GLFW, Nothing, Unit] = 231 | ZIO.accessM(_.get setErrorPrintStream printStream) 232 | 233 | final def setWindowTitle(window: Window, title: String): ZIO[GLFW, Nothing, Unit] = 234 | ZIO.accessM(_.get.setWindowTitle(window, title)) 235 | 236 | final val init: ZIO[GLFW, Nothing, Boolean] = 237 | ZIO.accessM(_.get.init) 238 | 239 | final val defaultWindowHints: ZIO[GLFW, Nothing, Unit] = 240 | ZIO.accessM(_.get.defaultWindowHints) 241 | 242 | final def windowHint(hint: Int, value: Int): ZIO[GLFW, Nothing, Unit] = 243 | ZIO.accessM(_.get.windowHint(hint, value)) 244 | 245 | final def createWindow( 246 | width: Int, 247 | height: Int, 248 | title: String, 249 | monitor: Long, 250 | share: Long 251 | ): ZIO[GLFW, Nothing, Window] = 252 | ZIO.accessM(_.get.createWindow(width, height, title, monitor, share)) 253 | 254 | final def maximizeWindow(window: Window): ZIO[GLFW, Nothing, Unit] = 255 | ZIO.accessM(_.get maximizeWindow window) 256 | 257 | final def setKeyCallback( 258 | window: Window, 259 | callback: (Window, Int, Int, Int, Int) => Unit 260 | ): ZIO[GLFW, Nothing, GLFWKeyCallback] = 261 | ZIO.accessM(_.get.setKeyCallback(window, callback)) 262 | 263 | final def freeCallbacks(window: Window): ZIO[GLFW, Nothing, Unit] = 264 | ZIO.accessM(_.get freeCallbacks window) 265 | 266 | final def destroyWindow(window: Window): ZIO[GLFW, Nothing, Unit] = 267 | ZIO.accessM(_.get destroyWindow window) 268 | 269 | final val terminate: ZIO[GLFW, Nothing, Unit] = 270 | ZIO.accessM(_.get.terminate) 271 | 272 | final def setWindowShouldClose(window: Window, value: Boolean): ZIO[GLFW, Nothing, Unit] = 273 | ZIO.accessM(_.get.setWindowShouldClose(window, value)) 274 | 275 | final def windowShouldClose(window: Window): ZIO[GLFW, Nothing, Boolean] = 276 | ZIO.accessM(_.get windowShouldClose window) 277 | 278 | final def makeContextCurrent(window: Window): ZIO[GLFW, Nothing, Unit] = 279 | ZIO.accessM(_.get makeContextCurrent window) 280 | 281 | final def swapInterval(interval: Int): ZIO[GLFW, Nothing, Unit] = 282 | ZIO.accessM(_.get swapInterval interval) 283 | 284 | final def showWindow(window: Window): ZIO[GLFW, Nothing, Unit] = 285 | ZIO.accessM(_.get showWindow window) 286 | 287 | final def swapBuffers(window: Window): ZIO[GLFW, Nothing, Unit] = 288 | ZIO.accessM(_.get swapBuffers window) 289 | 290 | final val pollEvents: ZIO[GLFW, Nothing, Unit] = 291 | ZIO.accessM(_.get.pollEvents) 292 | 293 | final def getKey(window: Window, key: Int): ZIO[GLFW, Nothing, Int] = 294 | ZIO.accessM(_.get.getKey(window, key)) 295 | 296 | final def getMouseButton(window: Window, button: Int): ZIO[GLFW, Nothing, Int] = 297 | ZIO.accessM(_.get.getMouseButton(window, button)) 298 | 299 | final def setInputMode(window: Window, mode: Int, value: Int): ZIO[GLFW, Nothing, Unit] = 300 | ZIO.accessM(_.get.setInputMode(window, mode, value)) 301 | 302 | final def getCursorPos(window: Window): ZIO[GLFW with Buffers, Nothing, CursorPos] = 303 | ZIO.accessM(_.get[GLFW.Service].getCursorPos(window)) 304 | 305 | final def setCursorPos(window: Window, xpos: Double, ypos: Double): ZIO[GLFW, Nothing, Unit] = 306 | ZIO.accessM(_.get.setCursorPos(window, xpos, ypos)) 307 | 308 | final def createStandardCursor(window: Window, shape: Int): ZIO[GLFW, Nothing, Cursor] = 309 | ZIO.accessM(_.get.createStandardCursor(window, shape)) 310 | 311 | final def setCursor(window: Window, cursor: Cursor): ZIO[GLFW, Nothing, Unit] = 312 | ZIO.accessM(_.get.setCursor(window, cursor)) 313 | 314 | final def getWindowSize(window: Window): ZIO[GLFW with Buffers, Nothing, WindowSize] = 315 | ZIO.accessM(_.get[GLFW.Service].getWindowSize(window)) 316 | } 317 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/glfw/types.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.glfw 2 | 3 | final case class Window(value: Long) extends AnyVal 4 | 5 | final case class WindowSize(width: Int, height: Int) 6 | 7 | final case class CursorPos(x: Double, y: Double) 8 | 9 | final case class Cursor(value: Long) extends AnyVal 10 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/images/Image.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.images 2 | 3 | import java.nio.ByteBuffer 4 | 5 | import org.lwjgl.opengl.GL11.{GL_RED, GL_RGB, GL_RGBA} 6 | 7 | final case class Image( 8 | image: ByteBuffer, 9 | width: Int, 10 | height: Int, 11 | components: Int 12 | ) { 13 | 14 | def format = components match { 15 | case 1 => GL_RED 16 | case 3 => GL_RGB 17 | case 4 => GL_RGBA 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/images/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core 2 | 3 | import java.nio.file.Path 4 | 5 | import org.lwjgl.stb.STBImage 6 | import org.lwjgl.system.MemoryStack 7 | import zio3d.engine.loaders.LoadingError.FileLoadError 8 | import zio.{Has, IO, ZIO, ZLayer} 9 | 10 | package object images { 11 | 12 | type Images = Has[Images.Service] 13 | 14 | object Images extends Serializable { 15 | 16 | trait Service { 17 | def loadImage(source: Path, flipVertical: Boolean, desiredChannels: Int): IO[FileLoadError, Image] 18 | } 19 | 20 | val live = ZLayer.succeed { 21 | new Service { 22 | override def loadImage(source: Path, flipVertical: Boolean, desiredChannels: Int): IO[FileLoadError, Image] = 23 | IO.effectTotal(MemoryStack.stackPush()).bracket(s => IO.effectTotal(s.close())) { stack => 24 | IO.effectTotal { 25 | val w = stack.mallocInt(1) 26 | val h = stack.mallocInt(1) 27 | val comp = stack.mallocInt(1) 28 | 29 | STBImage.stbi_set_flip_vertically_on_load(flipVertical) 30 | Option(STBImage.stbi_load(source.toString, w, h, comp, desiredChannels)) map { i => 31 | Image(i, w.get(), h.get(), comp.get()) 32 | } 33 | }.flatMap { 34 | case Some(i) => IO.succeed(i) 35 | case None => IO.fail(FileLoadError(source.toString, STBImage.stbi_failure_reason())) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | final val images: ZIO[Images, Nothing, Images.Service] = 43 | ZIO.access(_.get) 44 | 45 | final def loadImage(source: Path, flipVertical: Boolean, desiredChannels: Int): ZIO[Images, FileLoadError, Image] = 46 | ZIO.accessM(_.get.loadImage(source, flipVertical, desiredChannels)) 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/math/AxisAngle4.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.math 2 | 3 | final case class AxisAngle4( 4 | angle: Float, // radians 5 | x: Float, 6 | y: Float, 7 | z: Float 8 | ) { 9 | 10 | def normalize: AxisAngle4 = { 11 | val invLength = 1.0f / sqrt(x * x + y * y + z * z) 12 | AxisAngle4(angle, x * invLength, y * invLength, z * invLength) 13 | } 14 | 15 | def quaternion: Quaternion = { 16 | val s = sin(angle * 0.5f) 17 | val c = cos(angle * 0.5f) 18 | Quaternion( 19 | x * s, 20 | y * s, 21 | z * s, 22 | c 23 | ) 24 | } 25 | } 26 | 27 | object AxisAngle4 { 28 | private val `2PI` = Math.PI + Math.PI 29 | 30 | def apply(angle: Float, x: Float, y: Float, z: Float): AxisAngle4 = { 31 | val ang = (if (angle < 0.0) `2PI` + angle % `2PI` else angle.toDouble) % `2PI` 32 | new AxisAngle4(ang.toFloat, x, y, z) 33 | } 34 | 35 | def x(angle: Float): AxisAngle4 = 36 | apply(angle, 1, 0, 0) 37 | 38 | def y(angle: Float): AxisAngle4 = 39 | apply(angle, 0, 1, 0) 40 | 41 | def z(angle: Float): AxisAngle4 = 42 | apply(angle, 0, 0, 1) 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/math/Matrix4.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.math 2 | 3 | import zio3d.core.math 4 | 5 | final case class Matrix4( 6 | m00: Float, 7 | m01: Float, 8 | m02: Float, 9 | m03: Float, 10 | m10: Float, 11 | m11: Float, 12 | m12: Float, 13 | m13: Float, 14 | m20: Float, 15 | m21: Float, 16 | m22: Float, 17 | m23: Float, 18 | m30: Float, 19 | m31: Float, 20 | m32: Float, 21 | m33: Float 22 | ) { 23 | 24 | def *(m: Matrix4): Matrix4 = multiply(m) 25 | 26 | def multiply(m: Matrix4): Matrix4 = 27 | Matrix4( 28 | m00 * m.m00 + m10 * m.m01 + m20 * m.m02 + m30 * m.m03, 29 | m01 * m.m00 + m11 * m.m01 + m21 * m.m02 + m31 * m.m03, 30 | m02 * m.m00 + m12 * m.m01 + m22 * m.m02 + m32 * m.m03, 31 | m03 * m.m00 + m13 * m.m01 + m23 * m.m02 + m33 * m.m03, 32 | m00 * m.m10 + m10 * m.m11 + m20 * m.m12 + m30 * m.m13, 33 | m01 * m.m10 + m11 * m.m11 + m21 * m.m12 + m31 * m.m13, 34 | m02 * m.m10 + m12 * m.m11 + m22 * m.m12 + m32 * m.m13, 35 | m03 * m.m10 + m13 * m.m11 + m23 * m.m12 + m33 * m.m13, 36 | m00 * m.m20 + m10 * m.m21 + m20 * m.m22 + m30 * m.m23, 37 | m01 * m.m20 + m11 * m.m21 + m21 * m.m22 + m31 * m.m23, 38 | m02 * m.m20 + m12 * m.m21 + m22 * m.m22 + m32 * m.m23, 39 | m03 * m.m20 + m13 * m.m21 + m23 * m.m22 + m33 * m.m23, 40 | m00 * m.m30 + m10 * m.m31 + m20 * m.m32 + m30 * m.m33, 41 | m01 * m.m30 + m11 * m.m31 + m21 * m.m32 + m31 * m.m33, 42 | m02 * m.m30 + m12 * m.m31 + m22 * m.m32 + m32 * m.m33, 43 | m03 * m.m30 + m13 * m.m31 + m23 * m.m32 + m33 * m.m33 44 | ) 45 | 46 | def *(vec: Vector4): Vector4 = multiply(vec) 47 | 48 | def multiply(vec: Vector4): Vector4 = 49 | Vector4( 50 | m00 * vec.x + m10 * vec.y + m20 * vec.z + m30 * vec.w, 51 | m01 * vec.x + m11 * vec.y + m21 * vec.z + m31 * vec.w, 52 | m02 * vec.x + m12 * vec.y + m22 * vec.z + m32 * vec.w, 53 | m03 * vec.x + m13 * vec.y + m23 * vec.z + m33 * vec.w 54 | ) 55 | 56 | def translate(translation: Vector3): Matrix4 = 57 | this * Matrix4.forTranslation(translation) 58 | 59 | /** 60 | * @param angle in radians. 61 | * @return 62 | */ 63 | def rotateX(angle: Float): Matrix4 = { 64 | val sin = math.sin(angle) 65 | val cos = math.cos(angle) 66 | val rm11 = cos 67 | val rm12 = sin 68 | val rm21 = -sin 69 | val rm22 = cos 70 | 71 | // add temporaries for dependent values 72 | val nm10 = m10 * rm11 + m20 * rm12 73 | val nm11 = m11 * rm11 + m21 * rm12 74 | val nm12 = m12 * rm11 + m22 * rm12 75 | val nm13 = m13 * rm11 + m23 * rm12 76 | 77 | Matrix4( 78 | m00, 79 | m01, 80 | m02, 81 | m03, 82 | nm10, 83 | nm11, 84 | nm12, 85 | nm13, 86 | m10 * rm21 + m20 * rm22, 87 | m11 * rm21 + m21 * rm22, 88 | m12 * rm21 + m22 * rm22, 89 | m13 * rm21 + m23 * rm22, 90 | m30, 91 | m31, 92 | m32, 93 | m33 94 | ) 95 | } 96 | 97 | def rotateY(angle: Float): Matrix4 = { 98 | val sin = math.sin(angle) 99 | val cos = math.cos(angle) 100 | val rm00 = cos 101 | val rm02 = -sin 102 | val rm20 = sin 103 | val rm22 = cos 104 | 105 | // add temporaries for dependent values 106 | val nm00 = m00 * rm00 + m20 * rm02 107 | val nm01 = m01 * rm00 + m21 * rm02 108 | val nm02 = m02 * rm00 + m22 * rm02 109 | val nm03 = m03 * rm00 + m23 * rm02 110 | Matrix4( 111 | nm00, 112 | nm01, 113 | nm02, 114 | nm03, 115 | m10, 116 | m11, 117 | m12, 118 | m13, 119 | m00 * rm20 + m20 * rm22, 120 | m01 * rm20 + m21 * rm22, 121 | m02 * rm20 + m22 * rm22, 122 | m03 * rm20 + m23 * rm22, 123 | m30, 124 | m31, 125 | m32, 126 | m33 127 | ) 128 | } 129 | 130 | def rotateZ(angle: Float): Matrix4 = 131 | rotateTowardsXY(math.sin(angle), math.cos(angle)) 132 | 133 | def rotateTowardsXY(dirX: Float, dirY: Float): Matrix4 = { 134 | val rm00 = dirY 135 | val rm01 = dirX 136 | val rm10 = -dirX 137 | val rm11 = dirY 138 | val nm00 = m00 * rm00 + m10 * rm01 139 | val nm01 = m01 * rm00 + m11 * rm01 140 | val nm02 = m02 * rm00 + m12 * rm01 141 | val nm03 = m03 * rm00 + m13 * rm01 142 | Matrix4( 143 | nm00, 144 | nm01, 145 | nm02, 146 | nm03, 147 | m00 * rm10 + m10 * rm11, 148 | m01 * rm10 + m11 * rm11, 149 | m02 * rm10 + m12 * rm11, 150 | m03 * rm10 + m13 * rm11, 151 | m20, 152 | m21, 153 | m22, 154 | m23, 155 | m30, 156 | m31, 157 | m32, 158 | m33 159 | ) 160 | } 161 | 162 | def rotate(angle: Float, axis: Vector3): Matrix4 = 163 | rotate(angle, axis.x, axis.y, axis.z) 164 | 165 | def rotate(angle: Float, x: Float, y: Float, z: Float): Matrix4 = { 166 | val s = math.sin(angle) 167 | val c = cos(angle) 168 | val C = 1.0f - c 169 | val xx = x * x 170 | val xy = x * y 171 | val xz = x * z 172 | val yy = y * y 173 | val yz = y * z 174 | val zz = z * z 175 | val rm00 = xx * C + c 176 | val rm01 = xy * C + z * s 177 | val rm02 = xz * C - y * s 178 | val rm10 = xy * C - z * s 179 | val rm11 = yy * C + c 180 | val rm12 = yz * C + x * s 181 | val rm20 = xz * C + y * s 182 | val rm21 = yz * C - x * s 183 | val rm22 = zz * C + c 184 | val nm00 = m00 * rm00 + m10 * rm01 + m20 * rm02 185 | val nm01 = m01 * rm00 + m11 * rm01 + m21 * rm02 186 | val nm02 = m02 * rm00 + m12 * rm01 + m22 * rm02 187 | val nm03 = m03 * rm00 + m13 * rm01 + m23 * rm02 188 | val nm10 = m00 * rm10 + m10 * rm11 + m20 * rm12 189 | val nm11 = m01 * rm10 + m11 * rm11 + m21 * rm12 190 | val nm12 = m02 * rm10 + m12 * rm11 + m22 * rm12 191 | val nm13 = m03 * rm10 + m13 * rm11 + m23 * rm12 192 | 193 | Matrix4( 194 | nm00, 195 | nm01, 196 | nm02, 197 | nm03, 198 | nm10, 199 | nm11, 200 | nm12, 201 | nm13, 202 | m00 * rm20 + m10 * rm21 + m20 * rm22, 203 | m01 * rm20 + m11 * rm21 + m21 * rm22, 204 | m02 * rm20 + m12 * rm21 + m22 * rm22, 205 | m03 * rm20 + m13 * rm21 + m23 * rm22, 206 | m30, 207 | m31, 208 | m32, 209 | m33 210 | ) 211 | } 212 | 213 | def rotate(orientation: Quaternion): Matrix4 = 214 | this * Matrix4.forRotation(orientation) 215 | 216 | def scale(xyz: Float): Matrix4 = 217 | scale(xyz, xyz, xyz) 218 | 219 | def scale(x: Float, y: Float, z: Float): Matrix4 = 220 | this * Matrix4.forScale(Vector3(x, y, z)) 221 | 222 | def transpose3x3(dest: Matrix4): Matrix4 = { 223 | val nm00 = m00 224 | val nm01 = m10 225 | val nm02 = m20 226 | val nm10 = m01 227 | val nm11 = m11 228 | val nm12 = m21 229 | val nm20 = m02 230 | val nm21 = m12 231 | val nm22 = m22 232 | dest.copy( 233 | m00 = nm00, 234 | m01 = nm01, 235 | m02 = nm02, 236 | m10 = nm10, 237 | m11 = nm11, 238 | m12 = nm12, 239 | m20 = nm20, 240 | m21 = nm21, 241 | m22 = nm22 242 | ) 243 | } 244 | 245 | def invert(): Matrix4 = { 246 | val a = m00 * m11 - m01 * m10 247 | val b = m00 * m12 - m02 * m10 248 | val c = m00 * m13 - m03 * m10 249 | val d = m01 * m12 - m02 * m11 250 | val e = m01 * m13 - m03 * m11 251 | val f = m02 * m13 - m03 * m12 252 | val g = m20 * m31 - m21 * m30 253 | val h = m20 * m32 - m22 * m30 254 | val i = m20 * m33 - m23 * m30 255 | val j = m21 * m32 - m22 * m31 256 | val k = m21 * m33 - m23 * m31 257 | val l = m22 * m33 - m23 * m32 258 | 259 | // can get exception if denominator is 0 260 | val determinant = 1.0f / (a * l - b * k + c * j + d * i - e * h + f * g) 261 | val nm00 = (m11 * l - m12 * k + m13 * j) * determinant 262 | val nm01 = (-m01 * l + m02 * k - m03 * j) * determinant 263 | val nm02 = (m31 * f - m32 * e + m33 * d) * determinant 264 | val nm03 = (-m21 * f + m22 * e - m23 * d) * determinant 265 | val nm10 = (-m10 * l + m12 * i - m13 * h) * determinant 266 | val nm11 = (m00 * l - m02 * i + m03 * h) * determinant 267 | val nm12 = (-m30 * f + m32 * c - m33 * b) * determinant 268 | val nm13 = (m20 * f - m22 * c + m23 * b) * determinant 269 | val nm20 = (m10 * k - m11 * i + m13 * g) * determinant 270 | val nm21 = (-m00 * k + m01 * i - m03 * g) * determinant 271 | val nm22 = (m30 * e - m31 * c + m33 * a) * determinant 272 | val nm23 = (-m20 * e + m21 * c - m23 * a) * determinant 273 | val nm30 = (-m10 * j + m11 * h - m12 * g) * determinant 274 | val nm31 = (m00 * j - m01 * h + m02 * g) * determinant 275 | val nm32 = (-m30 * d + m31 * b - m32 * a) * determinant 276 | val nm33 = (m20 * d - m21 * b + m22 * a) * determinant 277 | 278 | Matrix4(nm00, nm01, nm02, nm03, nm10, nm11, nm12, nm13, nm20, nm21, nm22, nm23, nm30, nm31, nm32, nm33) 279 | } 280 | } 281 | 282 | object Matrix4 { 283 | 284 | lazy val identity = Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) 285 | 286 | def apply(): Matrix4 = identity 287 | 288 | def forTranslation(translation: Vector3): Matrix4 = 289 | forTranslation(translation.x, translation.y, translation.z) 290 | 291 | def forTranslation(x: Float, y: Float, z: Float): Matrix4 = 292 | Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1) 293 | 294 | def forScale(scale: Vector3): Matrix4 = 295 | Matrix4(scale.x, 0, 0, 0, 0, scale.y, 0, 0, 0, 0, scale.z, 0, 0, 0, 0, 1) 296 | 297 | def forRotation(q: Quaternion): Matrix4 = { 298 | val Quaternion(x, y, z, w) = q 299 | val x2 = x + x 300 | val y2 = y + y 301 | val z2 = z + z 302 | 303 | val xx = x * x2 304 | val xy = x * y2 305 | val xz = x * z2 306 | 307 | val yy = y * y2 308 | val yz = y * z2 309 | val zz = z * z2 310 | 311 | val wx = w * x2 312 | val wy = w * y2 313 | val wz = w * z2 314 | 315 | Matrix4( 316 | 1 - (yy + zz), 317 | xy + wz, 318 | xz - wy, 319 | 0, 320 | xy - wz, 321 | 1 - (xx + zz), 322 | yz + wx, 323 | 0, 324 | xz + wy, 325 | yz - wx, 326 | 1 - (xx + yy), 327 | 0, 328 | 0, 329 | 0, 330 | 0, 331 | 1 332 | ) 333 | } 334 | 335 | def forPerspective(fovRad: Float, aspect: Float, near: Float, far: Float): Matrix4 = { 336 | val fov = 1 / math.tan(fovRad / 2f) 337 | Matrix4( 338 | fov / aspect, 339 | 0, 340 | 0, 341 | 0, 342 | 0, 343 | fov, 344 | 0, 345 | 0, 346 | 0, 347 | 0, 348 | (far + near) / (near - far), 349 | -1, 350 | 0, 351 | 0, 352 | (2 * far * near) / (near - far), 353 | 0 354 | ) 355 | } 356 | 357 | def lookAt(eye: Vector3, center: Vector3, up: Vector3) = { 358 | val dx = eye.x - center.x 359 | val dy = eye.y - center.y 360 | val dz = eye.z - center.z 361 | 362 | val invDirLen = 1.0f / sqrt(dx * dx + dy * dy + dz * dz) 363 | val ndx = dx * invDirLen 364 | val ndy = dy * invDirLen 365 | val ndz = dz * invDirLen 366 | 367 | val lx = up.y * ndz - up.z * ndy 368 | val ly = up.z * ndx - up.x * ndz 369 | val lz = up.x * ndy - up.y * ndx 370 | 371 | val invLeftLen = 1.0f / sqrt(lx * lx + ly * ly + lz * lz) 372 | val nlx = lx * invLeftLen 373 | val nly = ly * invLeftLen 374 | val nlz = lz * invLeftLen 375 | 376 | val upnx = ndy * nlz - ndz * nly 377 | val upny = ndz * nlx - ndx * nlz 378 | val upnz = ndx * nly - ndy * nlx 379 | 380 | Matrix4( 381 | nlx, 382 | upnx, 383 | ndx, 384 | 0.0f, 385 | nly, 386 | upny, 387 | ndy, 388 | 0.0f, 389 | nlz, 390 | upnz, 391 | ndz, 392 | 0.0f, 393 | -(nlx * eye.x + nly * eye.y + nlz * eye.z), 394 | -(upnx * eye.x + upny * eye.y + upnz * eye.z), 395 | -(ndx * eye.x + ndy * eye.y + ndz * eye.z), 396 | 1.0f 397 | ) 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/math/Quaternion.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.math 2 | 3 | final case class Quaternion(x: Float, y: Float, z: Float, w: Float) { 4 | def vector: Vector4 = Vector4(x, y, z, w) 5 | 6 | def *(rhs: Quaternion): Quaternion = multiply(rhs) 7 | 8 | def multiply(rhs: Quaternion): Quaternion = 9 | Quaternion( 10 | this.w * rhs.x + this.x * rhs.w + this.y * rhs.z - this.z * rhs.y, 11 | this.w * rhs.y + this.y * rhs.w + this.z * rhs.x - this.x * rhs.z, 12 | this.w * rhs.z + this.z * rhs.w + this.x * rhs.y - this.y * rhs.x, 13 | this.w * rhs.w - this.x * rhs.x - this.y * rhs.y - this.z * rhs.z 14 | ) 15 | 16 | def transform(x: Float, y: Float, z: Float): Vector3 = { 17 | val w2 = this.w * this.w 18 | val x2 = this.x * this.x 19 | val y2 = this.y * this.y 20 | val z2 = this.z * this.z 21 | val zw = this.z * this.w 22 | val xy = this.x * this.y 23 | val xz = this.x * this.z 24 | val yw = this.y * this.w 25 | val yz = this.y * this.z 26 | val xw = this.x * this.w 27 | val m00 = w2 + x2 - z2 - y2 28 | val m01 = xy + zw + zw + xy 29 | val m02 = xz - yw + xz - yw 30 | val m10 = -zw + xy - zw + xy 31 | val m11 = y2 - z2 + w2 - x2 32 | val m12 = yz + yz + xw + xw 33 | val m20 = yw + xz + xz + yw 34 | val m21 = yz + yz - xw - xw 35 | val m22 = z2 - y2 - x2 + w2 36 | 37 | Vector3(m00 * x + m10 * y + m20 * z, m01 * x + m11 * y + m21 * z, m02 * x + m12 * y + m22 * z) 38 | } 39 | } 40 | 41 | object Quaternion { 42 | 43 | val Zero = Quaternion(0, 0, 0, 1) 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/math/Vector2.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.math 2 | 3 | final case class Vector2( 4 | x: Float, 5 | y: Float 6 | ) 7 | 8 | object Vector2 { 9 | def apply(): Vector2 = 10 | origin 11 | 12 | lazy val origin = Vector2(0, 0) 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/math/Vector3.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.math 2 | 3 | import zio3d.core.math 4 | 5 | final case class Vector3( 6 | x: Float, 7 | y: Float, 8 | z: Float 9 | ) { 10 | 11 | def +(vec: Vector3): Vector3 = 12 | add(vec) 13 | 14 | def add(vec: Vector3): Vector3 = 15 | Vector3(x + vec.x, y + vec.y, z + vec.z) 16 | 17 | def -(vec: Vector3): Vector3 = 18 | sub(vec) 19 | 20 | def sub(vec: Vector3): Vector3 = 21 | Vector3(x - vec.x, y - vec.y, z - vec.z) 22 | 23 | def *(vec: Vector3): Vector3 = 24 | mul(vec) 25 | 26 | def mul(vec: Vector3): Vector3 = 27 | Vector3(x * vec.x, y * vec.y, z * vec.z) 28 | 29 | def *(scalar: Float): Vector3 = 30 | mul(scalar) 31 | 32 | def mul(scalar: Float): Vector3 = 33 | Vector3(x * scalar, y * scalar, z * scalar) 34 | 35 | def cross(v: Vector3): Vector3 = 36 | Vector3(y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x) 37 | 38 | def rotate(quaternion: Quaternion): Vector3 = 39 | quaternion.transform(x, y, z) 40 | 41 | def lengthSquared: Float = 42 | x * x + y * y + z * z 43 | 44 | def length: Float = 45 | math.sqrt(lengthSquared) 46 | 47 | def normalize: Vector3 = { 48 | val invLength = 1.0f / length 49 | Vector3(x * invLength, y * invLength, z * invLength) 50 | } 51 | 52 | def dot(vec: Vector3) = 53 | x * vec.x + y * vec.y + z * vec.z 54 | } 55 | 56 | object Vector3 { 57 | def apply(): Vector3 = 58 | origin 59 | 60 | lazy val origin = Vector3(0, 0, 0) 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/math/Vector4.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.math 2 | 3 | final case class Vector4( 4 | x: Float, 5 | y: Float, 6 | z: Float, 7 | w: Float 8 | ) { 9 | 10 | def mul(mat: Matrix4): Vector4 = 11 | Vector4( 12 | mat.m00 * x + mat.m10 * y + mat.m20 * z + mat.m30 * w, 13 | mat.m01 * x + mat.m11 * y + mat.m21 * z + mat.m31 * w, 14 | mat.m02 * x + mat.m12 * y + mat.m22 * z + mat.m32 * w, 15 | w 16 | ) 17 | } 18 | 19 | object Vector4 { 20 | def apply(vector3: Vector3, w: Float): Vector4 = 21 | Vector4(vector3.x, vector3.y, vector3.z, w) 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/math/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core 2 | 3 | package object math { 4 | 5 | def abs(x: Int): Int = 6 | java.lang.Math.abs(x) 7 | 8 | def sin(x: Float): Float = 9 | java.lang.Math.sin(x.toDouble).toFloat 10 | 11 | def cos(x: Float): Float = 12 | java.lang.Math.cos(x.toDouble).toFloat 13 | 14 | def tan(x: Float): Float = 15 | java.lang.Math.tan(x.toDouble).toFloat 16 | 17 | def toRadians(x: Float): Float = 18 | java.lang.Math.toRadians(x.toDouble).toFloat 19 | 20 | def sqrt(x: Float): Float = 21 | java.lang.Math.sqrt(x.toDouble).toFloat 22 | 23 | def min(a: Float, b: Float): Float = 24 | java.lang.Math.min(a, b) 25 | 26 | def max(a: Float, b: Float): Float = 27 | java.lang.Math.max(a, b) 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/nvg/NVGContext.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core.nvg 2 | 3 | final case class NVGContext(value: Long) extends AnyVal 4 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/nvg/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.core 2 | 3 | import java.nio.ByteBuffer 4 | 5 | import org.lwjgl.nanovg.NVGColor 6 | import org.lwjgl.nanovg.NanoVG.{nvgBeginFrame, nvgBeginPath, nvgCircle, nvgCreateFontMem, nvgEndFrame, nvgFill, nvgFillColor, nvgFontFace, nvgFontSize, nvgRect, nvgText, nvgTextAlign} 7 | import org.lwjgl.nanovg.NanoVGGL3.nvgCreate 8 | import zio.{Has, UIO, ZLayer} 9 | 10 | package object nvg { 11 | 12 | type NVG = Has[NVG.Service] 13 | 14 | object NVG extends Serializable { 15 | 16 | trait Service { 17 | def create(flags: Int): UIO[NVGContext] 18 | 19 | def beginFrame(ctx: NVGContext, windowWidth: Int, windowHeight: Int, devicePixelRatio: Float): UIO[Unit] 20 | 21 | def endFrame(ctx: NVGContext): UIO[Unit] 22 | 23 | def createFontMem(ctx: NVGContext, name: String, data: ByteBuffer, freeData: Int): UIO[Int] 24 | 25 | def beginPath(ctx: NVGContext): UIO[Unit] 26 | 27 | def rect(ctx: NVGContext, x: Float, y: Float, w: Float, h: Float): UIO[Unit] 28 | 29 | def fillColor(ctx: NVGContext, color: NVGColor): UIO[Unit] 30 | 31 | def fill(ctx: NVGContext): UIO[Unit] 32 | 33 | def circle(ctx: NVGContext, cx: Float, cy: Float, radius: Float): UIO[Unit] 34 | 35 | def fontSize(ctx: NVGContext, size: Float): UIO[Unit] 36 | 37 | def fontFace(ctx: NVGContext, font: String): UIO[Unit] 38 | 39 | def textAlign(ctx: NVGContext, align: Int): UIO[Unit] 40 | 41 | def text(ctx: NVGContext, x: Float, y: Float, string: String): UIO[Float] 42 | } 43 | 44 | val live = ZLayer.succeed { 45 | new Service { 46 | def create(flags: Int): UIO[NVGContext] = 47 | UIO.effectTotal { NVGContext(nvgCreate(flags)) } 48 | 49 | def beginFrame(ctx: NVGContext, windowWidth: Int, windowHeight: Int, devicePixelRatio: Float): UIO[Unit] = 50 | UIO.effectTotal { nvgBeginFrame(ctx.value, windowWidth.toFloat, windowHeight.toFloat, devicePixelRatio) } 51 | 52 | def endFrame(ctx: NVGContext): UIO[Unit] = 53 | UIO.effectTotal { nvgEndFrame(ctx.value) } 54 | 55 | def createFontMem(ctx: NVGContext, name: String, data: ByteBuffer, freeData: Int): UIO[Int] = 56 | UIO.effectTotal { nvgCreateFontMem(ctx.value, name, data, freeData) } 57 | 58 | def beginPath(ctx: NVGContext): UIO[Unit] = 59 | UIO.effectTotal { nvgBeginPath(ctx.value) } 60 | 61 | def rect(ctx: NVGContext, x: Float, y: Float, w: Float, h: Float): UIO[Unit] = 62 | UIO.effectTotal { nvgRect(ctx.value, x, y, w, h) } 63 | 64 | def fillColor(ctx: NVGContext, color: NVGColor): UIO[Unit] = 65 | UIO.effectTotal { nvgFillColor(ctx.value, color) } 66 | 67 | def fill(ctx: NVGContext): UIO[Unit] = 68 | UIO.effectTotal { nvgFill(ctx.value) } 69 | 70 | def circle(ctx: NVGContext, cx: Float, cy: Float, radius: Float): UIO[Unit] = 71 | UIO.effectTotal { nvgCircle(ctx.value, cx, cy, radius) } 72 | 73 | def fontSize(ctx: NVGContext, size: Float): UIO[Unit] = 74 | UIO.effectTotal { nvgFontSize(ctx.value, size) } 75 | 76 | def fontFace(ctx: NVGContext, font: String): UIO[Unit] = 77 | UIO.effectTotal { nvgFontFace(ctx.value, font) } 78 | 79 | def textAlign(ctx: NVGContext, align: Int): UIO[Unit] = 80 | UIO.effectTotal { nvgTextAlign(ctx.value, align) } 81 | 82 | def text(ctx: NVGContext, x: Float, y: Float, string: String): UIO[Float] = 83 | UIO.effectTotal { nvgText(ctx.value, x, y, string) } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/core/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d 2 | 3 | import zio.ZLayer 4 | import zio3d.core.assimp.Assimp 5 | import zio3d.core.buffers.Buffers 6 | import zio3d.core.gl.GL 7 | import zio3d.core.glfw.GLFW 8 | import zio3d.core.images.Images 9 | import zio3d.core.nvg.NVG 10 | 11 | package object core { 12 | type CoreEnv = Buffers with GL with GLFW with NVG with Images with Assimp 13 | 14 | object CoreEnv { 15 | val live: ZLayer[Any, Nothing, CoreEnv] = 16 | Buffers.live ++ 17 | GL.live ++ 18 | (Buffers.live >>> GLFW.live) ++ 19 | NVG.live ++ 20 | Images.live ++ 21 | Assimp.live 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Box2D.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | /** 4 | * A 2D box defined by its corner (top-left) coordinates, width and height. 5 | */ 6 | final case class Box2D( 7 | x: Float, 8 | y: Float, 9 | width: Float, 10 | height: Float 11 | ) { 12 | 13 | def contains(x2: Float, y2: Float): Boolean = 14 | x2 >= x && 15 | y2 >= y && 16 | x2 < x + width && 17 | y2 < y + height 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Camera.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.math._ 4 | 5 | final case class Camera( 6 | position: Vector3, 7 | yaw: Float, // degrees 8 | pitch: Float // degrees 9 | ) { 10 | 11 | lazy val front: Vector3 = 12 | Vector3( 13 | cos(toRadians(yaw)) * cos(toRadians(pitch)), 14 | sin(toRadians(pitch)), 15 | sin(toRadians(yaw)) * cos(toRadians(pitch)) 16 | ).normalize 17 | 18 | lazy val right: Vector3 = 19 | front.cross(Vector3(0, 1, 0)).normalize 20 | 21 | lazy val up: Vector3 = 22 | right.cross(front).normalize 23 | 24 | lazy val viewMatrix: Matrix4 = 25 | Matrix4.lookAt(position, position + front, Vector3(0, 1, 0)) 26 | 27 | def withHeight(y: Float) = 28 | copy(position = Vector3(position.x, y, position.z)) 29 | 30 | def rotate(yawDelta: Float, pitchDelta: Float) = 31 | copy(pitch = pitch + pitchDelta, yaw = yaw + yawDelta) 32 | 33 | def movePosition(xDelta: Float, yDelta: Float, zDelta: Float) = 34 | copy( 35 | position = position + 36 | (front * zDelta) + 37 | (right * xDelta) + 38 | (up * yDelta) 39 | ) 40 | } 41 | 42 | object Camera { 43 | 44 | def apply(position: Vector3): Camera = 45 | Camera(position, 0, 0) 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Fire.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio.random.Random 4 | import zio.{random, ZIO} 5 | import zio3d.core.math.Vector3 6 | 7 | final case class FireSettings( 8 | maxParticles: Int, 9 | particleSpeed: Vector3, 10 | particleScale: Float, 11 | creationPeriodMillis: Long, 12 | ttl: Long, 13 | speedRndRange: Float, 14 | positionRndRange: Float, 15 | scaleRndRange: Float, 16 | animRange: Float 17 | ) 18 | 19 | final case class Fires( 20 | model: Model, // model for all fires 21 | textureAnimation: TextureAnimation, 22 | settings: FireSettings, 23 | fires: List[Fire] 24 | ) { 25 | def gameItem: GameItem = { 26 | val items = for { 27 | f <- fires 28 | p <- f.particles 29 | } yield p.item 30 | GameItem(model, items) 31 | } 32 | 33 | def update(now: Long, elapsedTime: Long): ZIO[Random, Nothing, Fires] = 34 | ZIO.foreach(fires)(_.update(settings, now, elapsedTime)) map { fs => 35 | copy(fires = fs.flatten) 36 | } 37 | 38 | def startFire(time: Long, position: Vector3): Fires = 39 | copy( 40 | fires = Fire( 41 | Particle( 42 | ItemInstance(position, settings.particleScale, textureAnimation), 43 | settings.particleSpeed, 44 | settings.ttl 45 | ), 46 | Nil, 47 | time, 48 | 0 49 | ) :: Nil 50 | ) 51 | 52 | def ++(o: Fires): Fires = 53 | copy(fires = fires ++ o.fires) 54 | } 55 | 56 | final case class Fire( 57 | baseParticle: Particle, 58 | particles: List[Particle], 59 | creationTime: Long, 60 | lastCreationTime: Long 61 | ) { 62 | 63 | def update(settings: FireSettings, now: Long, elapsedTime: Long): ZIO[Random, Nothing, Option[Fire]] = 64 | if (now > creationTime + settings.ttl) { 65 | if (particles.isEmpty) { 66 | ZIO.succeed(None) 67 | } else { 68 | ZIO.succeed( 69 | Some( 70 | copy( 71 | particles = updateParticles(elapsedTime) 72 | ) 73 | ) 74 | ) 75 | } 76 | } else { 77 | for { 78 | n <- createParticle(settings, now) 79 | } yield Some( 80 | copy( 81 | particles = n.toList ++ updateParticles(elapsedTime), 82 | lastCreationTime = 83 | if (n.isEmpty) lastCreationTime 84 | else now 85 | ) 86 | ) 87 | } 88 | 89 | private def updateParticles(elapsedTime: Long) = 90 | particles.foldLeft(List.empty[Particle]) { (ps, p) => 91 | p.update(elapsedTime).fold(ps)(_ :: ps) 92 | } 93 | 94 | def createParticle(settings: FireSettings, time: Long): ZIO[Random, Nothing, Option[Particle]] = 95 | if (time - lastCreationTime >= settings.creationPeriodMillis && particles.length < settings.maxParticles) { 96 | for { 97 | rs <- random.nextBoolean 98 | sign = if (rs) 1.0f else -1.0f 99 | speedInc <- random.nextFloat 100 | posInc <- random.nextFloat 101 | scaleInc <- random.nextFloat 102 | } yield Some( 103 | Particle.create( 104 | baseParticle, 105 | sign * posInc * settings.positionRndRange, 106 | sign * speedInc * settings.speedRndRange, 107 | sign * scaleInc * settings.scaleRndRange 108 | ) 109 | ) 110 | } else { 111 | ZIO.succeed(None) 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Fixtures.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | final case class Fixtures( 4 | lighting: LightSources, 5 | fog: Fog 6 | ) 7 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Fog.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.math.Vector3 4 | 5 | final case class Fog( 6 | active: Boolean, 7 | color: Vector3, 8 | density: Float 9 | ) 10 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/GameItem.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.math.{AxisAngle4, Matrix4, Quaternion, Vector3} 4 | 5 | final case class Model( 6 | meshes: List[Mesh], 7 | animation: Option[Animation] 8 | ) 9 | 10 | object Model { 11 | def still(mesh: Mesh): Model = 12 | Model(List(mesh), None) 13 | 14 | def still(meshes: List[Mesh]): Model = 15 | Model(meshes, None) 16 | 17 | def animated(meshes: List[Mesh], animation: Animation): Model = 18 | Model(meshes, Some(animation)) 19 | } 20 | 21 | final case class GameItem( 22 | model: Model, 23 | instances: List[ItemInstance] 24 | ) { 25 | 26 | def spawn(i: ItemInstance) = 27 | copy(instances = i :: instances) 28 | 29 | def animate = 30 | copy(instances = instances.map(_.animate)) 31 | } 32 | 33 | object GameItem { 34 | def apply(model: Model): GameItem = 35 | GameItem(model, Nil) 36 | } 37 | 38 | final case class ItemInstance( 39 | position: Vector3, 40 | scale: Float, 41 | rotation: Quaternion, 42 | boxSize: Float, // axis-aligned box for very simple collision detection 43 | modelAnimation: Option[ModelAnimation], 44 | textureAnimation: Option[TextureAnimation] 45 | ) { 46 | 47 | def withPosition(x: Float, y: Float, z: Float): ItemInstance = 48 | withPosition(Vector3(x, y, z)) 49 | 50 | def withPosition(position: Vector3): ItemInstance = 51 | copy(position = position) 52 | 53 | def withRotation(rotation: Quaternion) = 54 | copy(rotation = rotation) 55 | 56 | def withRotation(rotation: AxisAngle4) = 57 | copy(rotation = rotation.quaternion) 58 | 59 | def withScale(scale: Float) = 60 | copy(scale = scale) 61 | 62 | def withBoxSize(boxSize: Float) = 63 | copy(boxSize = boxSize) 64 | 65 | def animate = 66 | copy(modelAnimation = modelAnimation.map(_.animate)) 67 | 68 | def animateTexture(elapsedTime: Long) = 69 | textureAnimation.fold(this)(a => copy(textureAnimation = Some(a.animate(elapsedTime)))) 70 | 71 | def aabbContains(p: Vector3): Boolean = 72 | p.x >= (position.x - boxSize) && p.x <= (position.x + boxSize) && 73 | p.y >= (position.y - boxSize) && p.y <= (position.y + boxSize) && 74 | p.z >= (position.z - boxSize) && p.z <= (position.z + boxSize) 75 | } 76 | 77 | object ItemInstance { 78 | 79 | def apply(position: Vector3, scale: Float): ItemInstance = 80 | ItemInstance(position, scale, Quaternion.Zero, 0, None, None) 81 | 82 | def apply(position: Vector3, scale: Float, textureAnimation: TextureAnimation): ItemInstance = 83 | ItemInstance(position, scale, Quaternion.Zero, 0, None, Some(textureAnimation)) 84 | 85 | def apply(position: Vector3, scale: Float, rotation: Quaternion): ItemInstance = 86 | ItemInstance(position, scale, rotation, 0, None, None) 87 | } 88 | 89 | final case class AnimatedFrame( 90 | jointMatrices: Array[Matrix4] 91 | ) 92 | 93 | final case class Animation( 94 | name: String, 95 | frames: Array[AnimatedFrame], 96 | duration: Double = 0 97 | ) 98 | 99 | final case class ModelAnimation( 100 | frames: Int, 101 | currentFrame: Int 102 | ) { 103 | 104 | def animate: ModelAnimation = 105 | copy(currentFrame = (currentFrame + 1) % frames) 106 | } 107 | 108 | final case class TextureAnimation( 109 | cols: Int, 110 | rows: Int, 111 | textureUpdateMillis: Long, 112 | currentFrame: Int, 113 | lastTextureUpdateMillis: Long 114 | ) { 115 | 116 | private val frames = cols * rows 117 | 118 | def animate(elapsedTime: Long): TextureAnimation = 119 | if (lastTextureUpdateMillis + elapsedTime > textureUpdateMillis) { 120 | copy(currentFrame = (currentFrame + 1) % frames, lastTextureUpdateMillis = 0) 121 | } else { 122 | copy(lastTextureUpdateMillis = lastTextureUpdateMillis + elapsedTime) 123 | } 124 | } 125 | 126 | object TextureAnimation { 127 | def apply(cols: Int, rows: Int, updateMillis: Long): TextureAnimation = 128 | TextureAnimation(cols, rows, updateMillis, 0, 0) 129 | } 130 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Gun.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.math.Vector3 4 | 5 | final case class Gun( 6 | bulletModel: Model, 7 | baseParticle: Particle, 8 | particles: List[Particle], 9 | maxParticles: Int, 10 | firingRateMillis: Long, 11 | lastFireTime: Long, 12 | speed: Float 13 | ) { 14 | def renderItems: List[ItemInstance] = 15 | particles.map(_.item) 16 | 17 | def update(elapsedTime: Long): Gun = 18 | copy(particles = updateParticles(elapsedTime)) 19 | 20 | private def updateParticles(elapsedTime: Long) = 21 | particles.foldLeft(List.empty[Particle]) { (ps, p) => 22 | p.update(elapsedTime).fold(ps)(_ :: ps) 23 | } 24 | 25 | def fire(time: Long, from: Vector3, direction: Vector3): Gun = 26 | if (time - lastFireTime >= firingRateMillis && particles.length < maxParticles) { 27 | val newParticle = baseParticle.copy( 28 | item = baseParticle.item.withPosition(from), 29 | speed = direction * speed 30 | ) 31 | copy(particles = newParticle :: particles, lastFireTime = time) 32 | } else { 33 | this 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/HeightMapMesh.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.engine.HeightMapMesh.HeightArray 4 | 5 | final case class HeightMapMesh( 6 | minY: Float, 7 | maxY: Float, 8 | heightArray: HeightArray, 9 | mesh: Mesh 10 | ) { 11 | 12 | def getHeight(row: Int, col: Int): Float = 13 | if (row >= 0 && row < heightArray.length && 14 | col >= 0 && col < heightArray(row).length) { 15 | heightArray(row)(col) 16 | } else { 17 | 0 18 | } 19 | } 20 | 21 | object HeightMapMesh { 22 | type HeightArray = Array[Array[Float]] 23 | val MaxColour = 255f * 255f * 255f 24 | val StartX = -0.5f 25 | val StartZ = -0.5f 26 | 27 | def xLength = Math.abs(-StartX * 2) 28 | 29 | def zLength = Math.abs(-StartZ * 2) 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/LightSources.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.math.{Vector3, Vector4} 4 | 5 | final case class Attenuation( 6 | constant: Float, 7 | linear: Float, 8 | exponent: Float 9 | ) 10 | 11 | final case class PointLight( 12 | color: Vector3, 13 | position: Vector3, 14 | intensity: Float, 15 | attenuation: Attenuation 16 | ) { 17 | def toViewCoordinates(transformation: Transformation): PointLight = { 18 | val viewPos = Vector4(position.x, position.y, position.z, 1) mul transformation.viewMatrix 19 | copy(position = Vector3(viewPos.x, viewPos.y, viewPos.z)) 20 | } 21 | 22 | def withPosition(pos: Vector3): PointLight = 23 | copy(position = pos) 24 | } 25 | 26 | final case class DirectionalLight( 27 | color: Vector3, 28 | direction: Vector3, 29 | intensity: Float 30 | ) 31 | 32 | final case class SpotLight( 33 | pointLight: PointLight, 34 | coneDirection: Vector3, 35 | cutOff: Float 36 | ) { 37 | def toViewCoordinates(transformation: Transformation): SpotLight = { 38 | val dir = Vector4(coneDirection, 0) mul transformation.viewMatrix 39 | copy(coneDirection = Vector3(dir.x, dir.y, dir.z), pointLight = pointLight.toViewCoordinates(transformation)) 40 | } 41 | 42 | def withPosition(pos: Vector3) = 43 | copy(pointLight = pointLight.withPosition(pos)) 44 | 45 | def withDirection(dir: Vector3) = 46 | copy(coneDirection = dir) 47 | } 48 | 49 | final case class LightSources( 50 | ambient: Vector3, 51 | directionalLight: Option[DirectionalLight], 52 | point: List[PointLight], 53 | spot: List[SpotLight] 54 | ) 55 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Mesh.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.gl.{Texture, VertexArrayObject, VertexBufferObject} 4 | import zio3d.core.math.Vector4 5 | 6 | final case class Material( 7 | ambientColour: Vector4, 8 | diffuseColour: Vector4, 9 | specularColour: Vector4, 10 | reflectance: Float, 11 | texture: Option[Texture], 12 | normalMap: Option[Texture] 13 | ) 14 | 15 | object Material { 16 | def DefaultColour = Vector4(1.0f, 1.0f, 1.0f, 1.0f) 17 | 18 | def empty = 19 | new Material(DefaultColour, DefaultColour, DefaultColour, 0, None, None) 20 | 21 | def textured(texture: Texture) = 22 | new Material(DefaultColour, DefaultColour, DefaultColour, 0, Some(texture), None) 23 | 24 | def textured(texture: Texture, normalMap: Texture) = 25 | new Material(DefaultColour, DefaultColour, DefaultColour, 0, Some(texture), Some(normalMap)) 26 | } 27 | 28 | final case class Mesh( 29 | vao: VertexArrayObject, 30 | vbos: List[VertexBufferObject], 31 | vertexCount: Int, 32 | material: Material 33 | ) 34 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/MeshDefinition.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import java.nio.file.Path 4 | 5 | import zio3d.core.math.Vector4 6 | 7 | final case class TextureDefinition( 8 | path: Path, 9 | flipVertical: Boolean, 10 | cols: Int, 11 | rows: Int 12 | ) 13 | 14 | object TextureDefinition { 15 | def apply(path: Path): TextureDefinition = 16 | TextureDefinition(path, flipVertical = false) 17 | 18 | def apply(path: Path, flipVertical: Boolean): TextureDefinition = 19 | TextureDefinition(path, flipVertical, 1, 1) 20 | } 21 | 22 | final case class MaterialDefinition( 23 | ambientColour: Vector4, 24 | diffuseColour: Vector4, 25 | specularColour: Vector4, 26 | texture: Option[TextureDefinition], 27 | normalMap: Option[Path] 28 | ) 29 | 30 | object MaterialDefinition { 31 | def DefaultColour = Vector4(1.0f, 1.0f, 1.0f, 1.0f) 32 | 33 | def empty = 34 | new MaterialDefinition(DefaultColour, DefaultColour, DefaultColour, None, None) 35 | 36 | def textured(texture: Path, textureFlipVertical: Boolean) = 37 | new MaterialDefinition( 38 | DefaultColour, 39 | DefaultColour, 40 | DefaultColour, 41 | Some(TextureDefinition(texture, textureFlipVertical)), 42 | None 43 | ) 44 | 45 | def textured(texture: Path, normalMap: Option[Path], textureFlipVertical: Boolean) = 46 | new MaterialDefinition( 47 | DefaultColour, 48 | DefaultColour, 49 | DefaultColour, 50 | Some(TextureDefinition(texture, textureFlipVertical)), 51 | normalMap 52 | ) 53 | 54 | def animatedTextured(texture: Path, textureFlipVertical: Boolean, cols: Int, rows: Int) = 55 | new MaterialDefinition( 56 | DefaultColour, 57 | DefaultColour, 58 | DefaultColour, 59 | Some(TextureDefinition(texture, textureFlipVertical, cols, rows)), 60 | None 61 | ) 62 | } 63 | 64 | final case class MeshDefinition( 65 | positions: Array[Float], 66 | texCoords: Array[Float], 67 | normals: Array[Float], 68 | indices: Array[Int], 69 | weights: Array[Float], 70 | jointIndices: Array[Int], 71 | material: MaterialDefinition 72 | ) 73 | 74 | object MeshDefinition { 75 | val MaxWeights = 4 76 | 77 | def apply(positions: Array[Float]): MeshDefinition = 78 | MeshDefinition(positions, Array.empty, Array.empty, Array.empty, Array.empty, Array.empty, MaterialDefinition.empty) 79 | 80 | def apply( 81 | positions: Array[Float], 82 | texCoords: Array[Float], 83 | normals: Array[Float], 84 | indices: Array[Int], 85 | material: MaterialDefinition 86 | ): MeshDefinition = 87 | MeshDefinition( 88 | positions, 89 | texCoords, 90 | normals, 91 | indices, 92 | Array.fill(MaxWeights * positions.length / 3)(0.0f), 93 | Array.fill(MaxWeights * positions.length / 3)(0), 94 | material 95 | ) 96 | } 97 | 98 | final case class SimpleMeshDefinition( 99 | positions: Array[Float], 100 | texCoords: Array[Float], 101 | normals: Array[Float], 102 | indices: Array[Int], 103 | material: MaterialDefinition 104 | ) 105 | 106 | object SimpleMeshDefinition { 107 | def fromMeshDefinition(m: MeshDefinition) = 108 | SimpleMeshDefinition( 109 | m.positions, 110 | m.texCoords, 111 | m.normals, 112 | m.indices, 113 | m.material 114 | ) 115 | 116 | object Rectangle { 117 | val positions = 118 | Array( 119 | -0.5f, 0.5f, 0.0f, -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.5f, 0.5f, 0.0f 120 | ) 121 | 122 | val texCoords = 123 | Array( 124 | 0f, 1f, 0f, 0f, 1f, 0f, 1f, 1f 125 | ) 126 | 127 | val normals = 128 | Array( 129 | 0f, 0f, 1f, 0f, 0f, 1f, 0f, 0f, 1f, 0f, 0f, 1f 130 | ) 131 | 132 | val indices = 133 | Array( 134 | 0, 1, 3, 3, 1, 2 135 | ) 136 | } 137 | 138 | def image2D(textureFile: Path, flipVertical: Boolean): SimpleMeshDefinition = 139 | SimpleMeshDefinition( 140 | Rectangle.positions, 141 | Rectangle.texCoords, 142 | Rectangle.normals, 143 | Rectangle.indices, 144 | MaterialDefinition.textured(textureFile, flipVertical) 145 | ) 146 | 147 | def animatedImage2D(textureFile: Path, flipVertical: Boolean, cols: Int, rows: Int): SimpleMeshDefinition = 148 | SimpleMeshDefinition( 149 | Rectangle.positions, 150 | Rectangle.texCoords, 151 | Rectangle.normals, 152 | Rectangle.indices, 153 | MaterialDefinition.animatedTextured(textureFile, flipVertical, cols, rows) 154 | ) 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Particle.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.math.Vector3 4 | 5 | final case class Particle( 6 | item: ItemInstance, 7 | speed: Vector3, 8 | ttl: Long 9 | ) { 10 | 11 | def update(elapsedTime: Long): Option[Particle] = { 12 | val ttlRemaining = ttl - elapsedTime 13 | if (ttlRemaining > 0) { 14 | val delta = elapsedTime / 1000.0f 15 | val dx = speed.x * delta 16 | val dy = speed.y * delta 17 | val dz = speed.z * delta 18 | val pos = item.position 19 | 20 | Some( 21 | copy( 22 | item 23 | .withPosition(pos.x + dx, pos.y + dy, pos.z + dz) 24 | .animateTexture(elapsedTime), 25 | ttl = ttlRemaining 26 | ) 27 | ) 28 | } else { 29 | None 30 | } 31 | } 32 | } 33 | 34 | object Particle { 35 | def create( 36 | baseParticle: Particle, 37 | posInc: Float, 38 | speedInc: Float, 39 | scaleInc: Float 40 | ): Particle = { 41 | val item = baseParticle.item 42 | .withPosition(baseParticle.item.position + Vector3(posInc, posInc, posInc)) 43 | .withScale(baseParticle.item.scale * scaleInc) 44 | Particle(item, baseParticle.speed, baseParticle.ttl) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Perspective.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.glfw.WindowSize 4 | import zio3d.core.math.Matrix4 5 | 6 | final case class Perspective( 7 | fov: Float, 8 | zNear: Float, 9 | zFar: Float 10 | ) { 11 | 12 | def getTransformation(windowSize: WindowSize, camera: Camera): Transformation = { 13 | val aspectRatio = windowSize.width.toFloat / windowSize.height.toFloat 14 | Transformation( 15 | Matrix4.forPerspective(fov, aspectRatio, zNear, zFar), 16 | camera.viewMatrix 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/SkyboxDefinition.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import java.nio.file.Path 4 | 5 | final case class SkyboxDefinition( 6 | vertices: Array[Float], 7 | textureFront: Path, 8 | textureBack: Path, 9 | textureTop: Path, 10 | textureBottom: Path, 11 | textureLeft: Path, 12 | textureRight: Path, 13 | scale: Float 14 | ) { 15 | def textures = List(textureFront, textureBack, textureTop, textureBottom, textureRight, textureLeft) 16 | } 17 | 18 | object SkyboxDefinition { 19 | val vertices = Array( 20 | // positions 21 | -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 22 | -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 23 | -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 24 | -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 25 | -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 26 | 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 27 | 1.0f, -1.0f, 1.0f 28 | ) 29 | 30 | def apply( 31 | textureFront: Path, 32 | textureBack: Path, 33 | textureTop: Path, 34 | textureBottom: Path, 35 | textureLeft: Path, 36 | textureRight: Path, 37 | scale: Float 38 | ): SkyboxDefinition = 39 | new SkyboxDefinition( 40 | vertices, 41 | textureFront, 42 | textureBack, 43 | textureTop, 44 | textureBottom, 45 | textureLeft, 46 | textureRight, 47 | scale 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Terrain.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.math.{Vector2, Vector3} 4 | 5 | import scala.annotation.tailrec 6 | 7 | final case class Terrain( 8 | blocks: List[GameItem], 9 | boundingBoxes: Array[Array[Box2D]], 10 | heightMapMesh: HeightMapMesh, 11 | terrainSize: Int, 12 | verticesPerCol: Int, 13 | verticesPerRow: Int 14 | ) { 15 | 16 | def getPosition(x: Float, z: Float): Option[Vector3] = 17 | getPosition(Vector3(x, 0, z)) 18 | 19 | def getPosition(position: Vector2): Option[Vector3] = 20 | getPosition(Vector3(position.x, 0, position.y)) 21 | 22 | def getPosition(position: Vector3): Option[Vector3] = 23 | getHeight(position) map { h => 24 | Vector3(position.x, h, position.z) 25 | } 26 | 27 | /** 28 | * Return the height of the terrain at the given world position, or None if the position is not on the terrain. 29 | * @param position position. 30 | * @return height. 31 | */ 32 | def getHeight(position: Vector3): Option[Float] = { 33 | @tailrec 34 | def loop(row: Int, col: Int): Option[Float] = { 35 | val boundingBox = boundingBoxes(row)(col) 36 | if (boundingBox.contains(position.x, position.z)) { 37 | val terrainBlock = blocks(row * terrainSize + col) 38 | val triangle = getTriangle(position, boundingBox, terrainBlock) 39 | Some(interpolateHeight(triangle(0), triangle(1), triangle(2), position.x, position.z)) 40 | } else { 41 | if (col == 0) { 42 | if (row == 0) { 43 | None 44 | } else { 45 | loop(row - 1, terrainSize - 1) 46 | } 47 | } else { 48 | loop(row, col - 1) 49 | } 50 | } 51 | } 52 | 53 | loop(terrainSize - 1, terrainSize - 1) 54 | } 55 | 56 | private def getTriangle(position: Vector3, boundingBox: Box2D, terrainBlock: GameItem): Array[Vector3] = { 57 | val cellWidth = boundingBox.width / verticesPerCol.toFloat 58 | val cellHeight = boundingBox.height / verticesPerRow.toFloat 59 | val col = ((position.x - boundingBox.x) / cellWidth).toInt 60 | val row = ((position.z - boundingBox.y) / cellHeight).toInt 61 | 62 | val t1 = Vector3( 63 | boundingBox.x + col * cellWidth, 64 | getWorldHeight(row + 1, col, terrainBlock), 65 | boundingBox.y + (row + 1) * cellHeight 66 | ) 67 | val t2 = Vector3( 68 | boundingBox.x + (col + 1) * cellWidth, 69 | getWorldHeight(row, col + 1, terrainBlock), 70 | boundingBox.y + row * cellHeight 71 | ) 72 | val t0 = if (position.z < getDiagonalZCoord(t1.x, t1.z, t2.x, t2.z, position.x)) { 73 | Vector3(boundingBox.x + col * cellWidth, getWorldHeight(row, col, terrainBlock), boundingBox.y + row * cellHeight) 74 | } else { 75 | Vector3( 76 | boundingBox.x + (col + 1) * cellWidth, 77 | getWorldHeight(row + 2, col + 1, terrainBlock), 78 | boundingBox.y + (row + 1) * cellHeight 79 | ) 80 | } 81 | Array(t0, t1, t2) 82 | } 83 | 84 | private def getDiagonalZCoord(x1: Float, z1: Float, x2: Float, z2: Float, x: Float): Float = 85 | ((z1 - z2) / (x1 - x2)) * (x - x1) + z1 86 | 87 | private def getWorldHeight(row: Int, col: Int, gameItem: GameItem) = { 88 | val y = heightMapMesh.getHeight(row, col) 89 | y * gameItem.instances.head.scale + gameItem.instances.head.position.y 90 | } 91 | 92 | private def interpolateHeight(pA: Vector3, pB: Vector3, pC: Vector3, x: Float, z: Float) = { 93 | // Plane equation ax+by+cz+d=0 94 | val a = (pB.y - pA.y) * (pC.z - pA.z) - (pC.y - pA.y) * (pB.z - pA.z) 95 | val b = (pB.z - pA.z) * (pC.x - pA.x) - (pC.z - pA.z) * (pB.x - pA.x) 96 | val c = (pB.x - pA.x) * (pC.y - pA.y) - (pC.x - pA.x) * (pB.y - pA.y) 97 | val d = -(a * pA.x + b * pA.y + c * pA.z) 98 | // y = (-d -ax -cz) / b 99 | (-d - a * x - c * z) / b 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/Transformation.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import zio3d.core.math.Matrix4 4 | 5 | final case class Transformation( 6 | projectionMatrix: Matrix4, 7 | viewMatrix: Matrix4 8 | ) { 9 | 10 | def noTranslation: Transformation = 11 | copy(viewMatrix = viewMatrix.copy(m30 = 0, m31 = 0, m32 = 0)) 12 | 13 | def getModelMatrix(i: ItemInstance): Matrix4 = 14 | Matrix4.identity 15 | .translate(i.position) 16 | .rotate(i.rotation) 17 | .scale(i.scale) 18 | 19 | def getModelViewMatrix(i: ItemInstance): Matrix4 = 20 | viewMatrix * getModelMatrix(i) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/UserInput.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import org.lwjgl.glfw.GLFW._ 4 | import zio3d.core.glfw.CursorPos 5 | import zio3d.core.math.Vector2 6 | 7 | sealed abstract class Key(val value: Int) 8 | object Key { 9 | case object UP extends Key(GLFW_KEY_W) 10 | case object DOWN extends Key(GLFW_KEY_S) 11 | case object LEFT extends Key(GLFW_KEY_A) 12 | case object RIGHT extends Key(GLFW_KEY_D) 13 | case object ESC extends Key(GLFW_KEY_ESCAPE) 14 | 15 | val values: List[Key] = List(UP, DOWN, LEFT, RIGHT, ESC) 16 | } 17 | 18 | sealed abstract class MouseButton(val value: Int) 19 | object MouseButton { 20 | case object BUTTON_LEFT extends MouseButton(GLFW_MOUSE_BUTTON_LEFT) 21 | case object BUTTON_RIGHT extends MouseButton(GLFW_MOUSE_BUTTON_RIGHT) 22 | 23 | val values: List[MouseButton] = List(BUTTON_LEFT, BUTTON_RIGHT) 24 | } 25 | 26 | final case class UserInput( 27 | keys: Set[Key], 28 | mouseButtons: Set[MouseButton], 29 | prevCursor: CursorPos, 30 | currCursor: CursorPos 31 | ) { 32 | 33 | lazy val cursorMovement = 34 | Vector2((currCursor.x - prevCursor.x).toFloat, (currCursor.y - prevCursor.y).toFloat) 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/glwindow/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine 2 | 3 | import org.lwjgl.glfw.GLFW._ 4 | import org.lwjgl.system.MemoryUtil.NULL 5 | import zio.{Has, UIO, ZIO, ZLayer} 6 | import zio3d.core.glfw.{GLFW, Window} 7 | 8 | package object glwindow { 9 | 10 | type GLWindow = Has[GLWindow.Service] 11 | 12 | object GLWindow extends Serializable { 13 | trait Service { 14 | def open(title: String, width: Int, height: Int): UIO[Window] 15 | 16 | def close(w: Window): UIO[Unit] 17 | 18 | def getUserInput(w: Window, prevInput: Option[UserInput]): UIO[UserInput] 19 | } 20 | 21 | val live = ZLayer.fromService[GLFW.Service, GLWindow.Service] { glfw => 22 | new Service { 23 | 24 | def open(title: String, width: Int, height: Int) = 25 | for { 26 | _ <- initFramework 27 | w <- glfw.createWindow(width, height, title, NULL, NULL) 28 | _ <- glfw.makeContextCurrent(w) 29 | _ <- glfw.swapInterval(1) 30 | _ <- glfw.maximizeWindow(w) 31 | _ <- glfw.setInputMode(w, GLFW_CURSOR, GLFW_CURSOR_DISABLED) 32 | _ <- glfw.focusWindow(w) 33 | _ <- glfw.showWindow(w) 34 | } yield w 35 | 36 | private def initFramework: UIO[Unit] = 37 | glfw.setErrorPrintStream(System.err) *> 38 | glfw.init *> 39 | glfw.defaultWindowHints *> 40 | glfw.windowHint(GLFW_VISIBLE, GLFW_FALSE) *> 41 | glfw.windowHint(GLFW_SAMPLES, 4) *> 42 | glfw.windowHint(GLFW_CONTEXT_VERSION_MAJOR, 3) *> 43 | glfw.windowHint(GLFW_CONTEXT_VERSION_MINOR, 2) *> 44 | glfw.windowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE) *> 45 | glfw.windowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE) *> 46 | glfw.windowHint(GLFW_RESIZABLE, GLFW_TRUE) *> 47 | glfw.windowHint(GLFW_FOCUSED, GLFW_TRUE) 48 | 49 | def close(w: Window) = 50 | glfw.freeCallbacks(w) *> 51 | glfw.destroyWindow(w) *> 52 | glfw.terminate 53 | 54 | def getUserInput( 55 | w: Window, 56 | prevInput: Option[UserInput] 57 | ): UIO[UserInput] = 58 | for { 59 | keys <- getKeys(w) 60 | btns <- getMouseButtons(w) 61 | curs <- glfw.getCursorPos(w) 62 | } yield UserInput(keys, btns, prevInput.fold(curs)(_.currCursor), curs) 63 | 64 | private def getKeys(w: Window) = 65 | ZIO 66 | .foreach(Key.values) { k => 67 | glfw.getKey(w, k.value) map { e => 68 | if (e == 1) Some(k) else None 69 | } 70 | } 71 | .map(_.toSet.flatten) 72 | 73 | private def getMouseButtons(w: Window) = 74 | ZIO 75 | .foreach(MouseButton.values) { m => 76 | glfw.getMouseButton(w, m.value) map { e => 77 | if (e == GLFW_PRESS) Some(m) else None 78 | } 79 | } 80 | .map(_.toSet.flatten) 81 | } 82 | } 83 | } 84 | 85 | def open(title: String, width: Int, height: Int): ZIO[GLWindow, Nothing, Window] = 86 | ZIO.accessM(_.get.open(title, width, height)) 87 | 88 | def close(w: Window): ZIO[GLWindow, Nothing, Unit] = 89 | ZIO.accessM(_.get.close(w)) 90 | 91 | def getUserInput(window: Window, prevInput: Option[UserInput]): ZIO[GLWindow, Nothing, UserInput] = 92 | ZIO.accessM(_.get.getUserInput(window, prevInput)) 93 | } 94 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/loaders/LoadingError.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.loaders 2 | 3 | /** 4 | * An error may only occur during the loading of the game. 5 | */ 6 | sealed trait LoadingError 7 | 8 | object LoadingError { 9 | case class ShaderCompileError(message: String) extends LoadingError 10 | case class ProgramLinkError(message: String) extends LoadingError 11 | case class FileLoadError(filename: String, message: String) extends LoadingError 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/loaders/assimp/anim/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.loaders.assimp 2 | 3 | import java.nio.file.Path 4 | 5 | import org.lwjgl.assimp.Assimp._ 6 | import org.lwjgl.assimp._ 7 | import zio.stream.{Sink, Stream} 8 | import zio._ 9 | import zio3d.core.assimp.Assimp 10 | import zio3d.core.math.{Matrix4, Quaternion} 11 | import zio3d.engine.loaders.LoadingError 12 | import zio3d.engine._ 13 | 14 | import scala.jdk.CollectionConverters._ 15 | 16 | package object anim { 17 | 18 | type AnimMeshLoader = Has[AnimMeshLoader.Service] 19 | 20 | object AnimMeshLoader extends Serializable { 21 | trait Service { 22 | def load(resourcePath: Path): IO[LoadingError, AnimMesh] 23 | 24 | def load(resourcePath: Path, flags: Int): IO[LoadingError, AnimMesh] 25 | } 26 | 27 | val live = ZLayer.fromService[Assimp.Service, AnimMeshLoader.Service] { assimp => 28 | new Service { 29 | 30 | override def load(resourcePath: Path): IO[LoadingError, AnimMesh] = 31 | load( 32 | resourcePath, 33 | aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices | aiProcess_Triangulate 34 | | aiProcess_FixInfacingNormals | aiProcess_LimitBoneWeights 35 | ) 36 | 37 | override def load(resourcePath: Path, flags: Int): IO[LoadingError, AnimMesh] = 38 | for { 39 | s <- assimp.importFile(resourcePath, flags) 40 | 41 | aiMaterials = s.mMaterials() 42 | materials <- Stream 43 | .range(0, s.mNumMaterials()) 44 | .mapM(i => processMaterial(AIMaterial.create(aiMaterials.get(i)), resourcePath.getParent)) 45 | .run(Sink.collectAll[MaterialDefinition]) 46 | 47 | aiMeshes = s.mMeshes() 48 | boneCounter <- Ref.make(0) 49 | meshBones <- Stream 50 | .range(0, s.mNumMeshes()) 51 | .mapM(i => processMesh(s, AIMesh.create(aiMeshes.get(i)), materials.toList, boneCounter)) 52 | .run(Sink.collectAll[MeshBones]) 53 | 54 | bones = meshBones.flatMap(_.bones) 55 | // rootNode <- processAnimations(s, bones) 56 | rootTrans = toMatrix(s.mRootNode().mTransformation()) 57 | animations <- processAnimations(s, bones, rootTrans) 58 | 59 | } yield AnimMesh(meshBones.map(_.mesh).toList, animations) 60 | 61 | private def processMaterial(material: AIMaterial, parentPath: Path) = 62 | for { 63 | t <- assimp.getMaterialTexture(material, aiTextureType_DIFFUSE) 64 | a <- assimp.getMaterialColor(material, AI_MATKEY_COLOR_AMBIENT, aiTextureType_NONE) 65 | d <- assimp.getMaterialColor(material, AI_MATKEY_COLOR_DIFFUSE, aiTextureType_NONE) 66 | s <- assimp.getMaterialColor(material, AI_MATKEY_COLOR_SPECULAR, aiTextureType_NONE) 67 | } yield MaterialDefinition(a, d, s, t.map(p => TextureDefinition(parentPath.resolve(p))), None) 68 | 69 | private def processMesh( 70 | scene: AIScene, 71 | mesh: AIMesh, 72 | materials: List[MaterialDefinition], 73 | boneCounter: Ref[Int] 74 | ) = 75 | for { 76 | v <- toVectorArray(mesh.mVertices()) 77 | n <- toVectorArray(mesh.mNormals()) 78 | t <- getTextureCoords(mesh) 79 | i <- getIndices(mesh) 80 | b <- getBones(mesh, boneCounter) 81 | bw <- getBoneIdsAndWeights(b, mesh.mNumVertices()) 82 | 83 | im = mesh.mMaterialIndex() 84 | mat = if (im >= 0 && im < materials.length) materials(im) else MaterialDefinition.empty 85 | } yield MeshBones( 86 | MeshDefinition(v.toArray, t.toArray, n.toArray, i.toArray, bw._2.toArray, bw._1.toArray, mat), 87 | b.toList 88 | ) 89 | 90 | private def toVectorArray(buffer: AIVector3D.Buffer) = 91 | Stream 92 | .fromIterable[AIVector3D](buffer.iterator().asScala.iterator.to(Iterable)) 93 | .mapConcatChunk(v => Chunk(v.x(), v.y(), v.z())) 94 | .run(Sink.collectAll[Float]) 95 | 96 | private def getTextureCoords(mesh: AIMesh) = 97 | Stream 98 | .fromIterable[AIVector3D](mesh.mTextureCoords(0).iterator().asScala.to(Iterable)) 99 | .mapConcatChunk(c => Chunk(c.x(), 1 - c.y())) 100 | .run(Sink.collectAll[Float]) 101 | 102 | private def getIndices(mesh: AIMesh) = 103 | Stream 104 | .fromIterable[AIFace](mesh.mFaces().iterator().asScala.to(Iterable)) 105 | .map(_.mIndices()) 106 | .flatMap(buf => Stream.unfold(buf)(b => if (b.hasRemaining) Some((b.get(), b)) else None)) 107 | .run(Sink.collectAll[Int]) 108 | 109 | private def getBones(mesh: AIMesh, boneCounter: Ref[Int]): UIO[Chunk[Bone]] = 110 | Stream 111 | .range(0, mesh.mNumBones()) 112 | .mapM( 113 | i => 114 | for { 115 | id <- boneCounter.get 116 | b <- createBone(id, AIBone.create(mesh.mBones().get(i))) 117 | _ <- boneCounter.update(_ + 1) 118 | } yield b 119 | ) 120 | .run(Sink.collectAll[Bone]) 121 | 122 | private def getBoneIdsAndWeights(bones: Chunk[Bone], numVertices: Int) = { 123 | def getWeights(vertexWeightMap: Map[Int, Chunk[VertexWeight]]): Seq[Float] = 124 | for { 125 | i <- 0 until numVertices 126 | j <- 0 until MeshDefinition.MaxWeights 127 | vertexWeights = vertexWeightMap.getOrElse(i, Nil) 128 | n = vertexWeights.length 129 | } yield if (j < n) vertexWeights(j).weight else 0.0f 130 | 131 | def getBoneIds(vertexWeightMap: Map[Int, Chunk[VertexWeight]]): Seq[Int] = 132 | for { 133 | i <- 0 until numVertices 134 | j <- 0 until MeshDefinition.MaxWeights 135 | vertexWeights = vertexWeightMap.getOrElse(i, Nil) 136 | n = vertexWeights.length 137 | } yield if (j < n) vertexWeights(j).boneId else 0 138 | 139 | for { 140 | ws <- Stream 141 | .fromIterable(bones) 142 | .mapConcatChunk(b => Chunk.fromIterable(b.weights)) 143 | .run(Sink.collectAll[VertexWeight]) 144 | .map(_.groupBy(_.vertexId)) 145 | } yield (getBoneIds(ws), getWeights(ws)) 146 | } 147 | 148 | private def createBone(id: Int, bone: AIBone) = 149 | for { 150 | ws <- Stream 151 | .fromIterable[AIVertexWeight](bone.mWeights().iterator().asScala.to(Iterable)) 152 | .map(w => VertexWeight(id, w.mVertexId(), w.mWeight())) 153 | .run(Sink.collectAll[VertexWeight]) 154 | } yield Bone(id, bone.mName().dataString(), toMatrix(bone.mOffsetMatrix()), ws.toList) 155 | 156 | private def processNodeHierarchy(aiNode: AINode, parentNode: Option[Node]): UIO[Node] = 157 | for { 158 | n <- UIO.succeed(Node(aiNode.mName().dataString(), parentNode)) 159 | c <- UIO.foreach(List.range(0, aiNode.mNumChildren()))( 160 | i => processNodeHierarchy(AINode.create(aiNode.mChildren().get(i)), Some(n)) 161 | ) 162 | } yield n.copy(children = c) 163 | 164 | private def processAnimations(scene: AIScene, bones: Chunk[Bone], rootTrans: Matrix4) = { 165 | val aiRootNode = scene.mRootNode() 166 | for { 167 | rootNode <- processNodeHierarchy(aiRootNode, None) 168 | animations <- UIO.foreach(List.range(0, scene.mNumAnimations()))( 169 | i => buildAnimation(AIAnimation.create(scene.mAnimations().get(i)), bones.toList, rootTrans, rootNode) 170 | ) 171 | } yield animations 172 | } 173 | 174 | private def buildAnimation(a: AIAnimation, bones: List[Bone], rootTrans: Matrix4, rootNode: Node) = 175 | for { 176 | _ <- populateNodes(a, rootNode) 177 | frames <- buildAnimationFrames(bones, rootNode, rootTrans) 178 | } yield Animation(a.mName().dataString(), frames.toArray, a.mDuration()) 179 | 180 | private def populateNodes(anim: AIAnimation, rootNode: Node) = 181 | Stream 182 | .range(0, anim.mNumChannels()) 183 | .map(i => AINodeAnim.create(anim.mChannels().get(i))) 184 | .mapM { nodeAnim => 185 | getTransformationMatrices(nodeAnim) map { mats => 186 | rootNode.findByName(nodeAnim.mNodeName().dataString()) map { n => 187 | n.addTransformations(mats) 188 | } 189 | } 190 | } 191 | .run(Sink.drain) 192 | 193 | private def getTransformationMatrices(nodeAnim: AINodeAnim) = 194 | Stream 195 | .range(0, nodeAnim.mNumPositionKeys()) 196 | .map { i => 197 | val posVec = nodeAnim.mPositionKeys().get(i).mValue() 198 | val transMat = Matrix4.forTranslation(posVec.x(), posVec.y(), posVec.z()) 199 | 200 | val rotQuat = nodeAnim.mRotationKeys().get(i).mValue() 201 | val quat = Quaternion(rotQuat.x(), rotQuat.y(), rotQuat.z(), rotQuat.w()) 202 | 203 | if (i < nodeAnim.mNumScalingKeys()) { 204 | val scaleVec = nodeAnim.mScalingKeys().get(i).mValue() 205 | transMat 206 | .rotate(quat) 207 | .scale(scaleVec.x(), scaleVec.y(), scaleVec.z()) 208 | } else { 209 | transMat 210 | .rotate(quat) 211 | } 212 | } 213 | .run(Sink.collectAll[Matrix4]) 214 | 215 | private def buildAnimationFrames(bones: List[Bone], rootNode: Node, rootTransform: Matrix4) = { 216 | def getFrame(i: Int) = { 217 | val matrices = bones.map { b => 218 | val node = rootNode.findByName(b.boneName) 219 | val boneMatrix = Node.getParentTransforms(node, i) 220 | rootTransform * boneMatrix * b.offset 221 | } 222 | AnimatedFrame(matrices.toArray) 223 | } 224 | 225 | val numFrames = rootNode.getAnimationFrames() 226 | Stream 227 | .range(0, numFrames) 228 | .map(i => getFrame(i)) 229 | .run(Sink.collectAll[AnimatedFrame]) 230 | } 231 | 232 | private def toMatrix(m: AIMatrix4x4) = 233 | Matrix4( 234 | m.a1(), 235 | m.b1(), 236 | m.c1(), 237 | m.d1(), 238 | m.a2(), 239 | m.b2(), 240 | m.c2(), 241 | m.d2(), 242 | m.a3(), 243 | m.b3(), 244 | m.c3(), 245 | m.d3(), 246 | m.a4(), 247 | m.b4(), 248 | m.c4(), 249 | m.d4() 250 | ) 251 | } 252 | } 253 | } 254 | 255 | final def loadAnimMesh(resourcePath: Path): ZIO[AnimMeshLoader, LoadingError, AnimMesh] = 256 | ZIO.accessM(_.get.load(resourcePath)) 257 | 258 | } 259 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/loaders/assimp/anim/types.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.loaders.assimp.anim 2 | 3 | import zio.Chunk 4 | import zio3d.core.math.Matrix4 5 | import zio3d.engine.{Animation, MeshDefinition} 6 | 7 | import scala.collection.mutable.ListBuffer 8 | 9 | final case class AnimMesh( 10 | meshes: List[MeshDefinition], 11 | animations: List[Animation] 12 | ) 13 | 14 | final case class MeshBones( 15 | mesh: MeshDefinition, 16 | bones: List[Bone] 17 | ) 18 | 19 | final case class VertexWeight( 20 | boneId: Int, 21 | vertexId: Int, 22 | weight: Float 23 | ) 24 | 25 | final case class Bone( 26 | boneId: Int, 27 | boneName: String, 28 | offset: Matrix4, 29 | weights: List[VertexWeight] 30 | ) 31 | 32 | 33 | final case class Node( 34 | name: String, 35 | parent: Option[Node], 36 | children: List[Node] = List.empty, 37 | transformations: ListBuffer[Matrix4] = new ListBuffer() 38 | ) { 39 | 40 | def findByName(targetName: String): Option[Node] = 41 | if (targetName == name) { 42 | Some(this) 43 | } else { 44 | children.map(_.findByName(targetName)).find(_.isDefined).flatten 45 | } 46 | 47 | def getAnimationFrames(): Int = { 48 | val numFrames = transformations.size 49 | children.foldLeft(numFrames)((f, c) => Math.max(c.getAnimationFrames(), f)) 50 | } 51 | 52 | def addTransformations(ts: Chunk[Matrix4]) = { 53 | transformations ++= ts 54 | this 55 | } 56 | } 57 | 58 | object Node { 59 | def getParentTransforms(node: Option[Node], framePos: Int): Matrix4 = 60 | node match { 61 | case None => Matrix4.identity 62 | case Some(n) => 63 | val parentTransform = getParentTransforms(n.parent, framePos) 64 | val transformations = n.transformations.toList 65 | val transSize = transformations.length 66 | val nodeTransform = if (framePos < transSize) { 67 | transformations(framePos) 68 | } else if (transSize > 0) { 69 | transformations(transSize - 1) 70 | } else { 71 | Matrix4.identity 72 | } 73 | parentTransform * nodeTransform 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/loaders/assimp/static/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.loaders.assimp 2 | 3 | import java.nio.file.Path 4 | 5 | import org.lwjgl.assimp.Assimp._ 6 | import org.lwjgl.assimp.{AIFace, AIMaterial, AIMesh, AIVector3D} 7 | import zio._ 8 | import zio.stream.{Sink, Stream} 9 | import zio3d.core.assimp.Assimp 10 | import zio3d.engine.loaders.LoadingError 11 | import zio3d.engine.{MaterialDefinition, MeshDefinition, TextureDefinition} 12 | 13 | import scala.jdk.CollectionConverters._ 14 | 15 | package object static { 16 | 17 | type StaticMeshLoader = Has[StaticMeshLoader.Service] 18 | 19 | object StaticMeshLoader extends Serializable { 20 | trait Service { 21 | def load(resourcePath: Path): IO[LoadingError, List[MeshDefinition]] 22 | 23 | def load(resourcePath: Path, flags: Int): IO[LoadingError, List[MeshDefinition]] 24 | } 25 | 26 | val live = ZLayer.fromService[Assimp.Service, StaticMeshLoader.Service] { assimp => 27 | new Service { 28 | 29 | override def load(resourcePath: Path): IO[LoadingError, List[MeshDefinition]] = 30 | load( 31 | resourcePath, 32 | aiProcess_GenSmoothNormals | 33 | aiProcess_JoinIdenticalVertices | 34 | aiProcess_Triangulate | 35 | aiProcess_FixInfacingNormals 36 | ) 37 | 38 | override def load(resourcePath: Path, flags: Int): IO[LoadingError, List[MeshDefinition]] = 39 | for { 40 | s <- assimp.importFile(resourcePath, flags) 41 | 42 | aiMaterials = s.mMaterials() 43 | materials <- Stream 44 | .range(0, s.mNumMaterials()) 45 | .mapM(i => processMaterial(AIMaterial.create(aiMaterials.get(i)), resourcePath.getParent)) 46 | .run(Sink.collectAll[MaterialDefinition]) 47 | 48 | aiMeshes = s.mMeshes() 49 | meshes <- Stream 50 | .range(0, s.mNumMeshes()) 51 | .mapM(i => processMesh(AIMesh.create(aiMeshes.get(i)), materials.toList)) 52 | .run(Sink.collectAll[MeshDefinition]) 53 | } yield meshes.toList 54 | 55 | private def processMaterial(material: AIMaterial, path: Path): IO[LoadingError, MaterialDefinition] = 56 | for { 57 | t <- assimp.getMaterialTexture(material, aiTextureType_DIFFUSE) 58 | a <- assimp.getMaterialColor(material, AI_MATKEY_COLOR_AMBIENT, aiTextureType_NONE) 59 | d <- assimp.getMaterialColor(material, AI_MATKEY_COLOR_DIFFUSE, aiTextureType_NONE) 60 | s <- assimp.getMaterialColor(material, AI_MATKEY_COLOR_SPECULAR, aiTextureType_NONE) 61 | } yield MaterialDefinition(a, d, s, t.map(p => TextureDefinition(path.resolve(p))), None) 62 | 63 | private def processMesh(mesh: AIMesh, materials: List[MaterialDefinition]) = 64 | for { 65 | v <- toVectorArray(mesh.mVertices()) 66 | n <- toVectorArray(mesh.mNormals()) 67 | t <- getTextureCoords(mesh) 68 | i <- getIndices(mesh) 69 | im = mesh.mMaterialIndex() 70 | mat = if (im >= 0 && im < materials.length) materials(im) else MaterialDefinition.empty 71 | } yield MeshDefinition(v.toArray, t.toArray, n.toArray, i.toArray, mat) 72 | 73 | private def toVectorArray(buffer: AIVector3D.Buffer) = 74 | Stream 75 | .fromIterable[AIVector3D](buffer.iterator().asScala.to(Iterable)) 76 | .mapConcatChunk(v => Chunk(v.x(), v.y(), v.z())) 77 | .run(Sink.collectAll[Float]) 78 | 79 | private def getTextureCoords(mesh: AIMesh) = 80 | Stream 81 | .fromIterable[AIVector3D](mesh.mTextureCoords(0).iterator().asScala.to(Iterable)) 82 | .mapConcatChunk(c => Chunk(c.x(), 1 - c.y())) 83 | .run(Sink.collectAll[Float]) 84 | 85 | private def getIndices(mesh: AIMesh) = 86 | Stream 87 | .fromIterable[AIFace](mesh.mFaces().iterator().asScala.to(Iterable)) 88 | .map(_.mIndices()) 89 | .flatMap(buf => Stream.unfold(buf)(b => if (b.hasRemaining) Some((b.get(), b)) else None)) 90 | .run(Sink.collectAll[Int]) 91 | } 92 | } 93 | } 94 | 95 | final def loadStaticMesh(resourcePath: Path): ZIO[StaticMeshLoader, LoadingError, List[MeshDefinition]] = 96 | ZIO.accessM(_.get.load(resourcePath)) 97 | 98 | final def loadStaticMesh(resourcePath: Path, flags: Int): ZIO[StaticMeshLoader, LoadingError, List[MeshDefinition]] = 99 | ZIO.accessM(_.get.load(resourcePath, flags)) 100 | } 101 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/loaders/heightmap/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.loaders 2 | 3 | import java.nio.ByteBuffer 4 | 5 | import zio.{Has, IO, ZIO, ZLayer} 6 | import zio3d.core.images.Image 7 | import zio3d.core.math.Vector3 8 | import zio3d.engine.HeightMapMesh.{StartX, StartZ, xLength, zLength} 9 | import zio3d.engine.shaders.scene.{SceneShaderInterpreter, SceneShaderProgram} 10 | import zio3d.engine.{HeightMapMesh, MaterialDefinition, MeshDefinition, ShaderEnv} 11 | 12 | import scala.collection.mutable.ListBuffer 13 | 14 | package object heightmap { 15 | 16 | type HeightMapLoader = Has[HeightMapLoader.Service] 17 | 18 | object HeightMapLoader extends Serializable { 19 | trait Service { 20 | def load( 21 | program: SceneShaderProgram, 22 | minY: Float, 23 | maxY: Float, 24 | heightMapImage: Image, 25 | materialDefinition: MaterialDefinition, 26 | textInc: Int 27 | ): IO[LoadingError, HeightMapMesh] 28 | } 29 | 30 | val live = ZLayer.fromFunction[ShaderEnv, HeightMapLoader.Service] { env => 31 | new Service { 32 | 33 | private val sceneShaderInterpreter = env.get[SceneShaderInterpreter.Service] 34 | 35 | override def load( 36 | program: SceneShaderProgram, 37 | minY: Float, 38 | maxY: Float, 39 | heightMapImage: Image, 40 | materialDefinition: MaterialDefinition, 41 | textInc: Int 42 | ): IO[LoadingError, HeightMapMesh] = { 43 | val width = heightMapImage.width 44 | val height = heightMapImage.height 45 | val incX = xLength / (width - 1) 46 | val incZ = zLength / (height - 1) 47 | 48 | def getPosition(row: Int, col: Int) = { 49 | val currentHeight = getHeight(col, row, width, minY, maxY, heightMapImage.image) 50 | List(StartX + col * incX, currentHeight, StartZ + row * incZ) 51 | } 52 | 53 | def getTexCoords(row: Int, col: Int) = 54 | List(textInc.toFloat * col.toFloat / width.toFloat, textInc.toFloat * row.toFloat / height.toFloat) 55 | 56 | def getIndices(row: Int, col: Int) = 57 | if (col < width - 1 && row < height - 1) { 58 | val leftTop = row * width + col 59 | val leftBottom = (row + 1) * width + col 60 | val rightBottom = (row + 1) * width + col + 1 61 | val rightTop = row * width + col + 1 62 | List(rightTop, leftBottom, rightBottom, leftTop, leftBottom, rightTop) 63 | } else { 64 | Nil 65 | } 66 | 67 | def getMeshInput(materialDefinition: MaterialDefinition) = { 68 | val positions = ListBuffer[Float]() 69 | val textCoords = ListBuffer[Float]() 70 | val indices = ListBuffer[Int]() 71 | val heightArray = Array.ofDim[Float](height, width) 72 | val normals = ListBuffer[Float]() 73 | for (row <- 0 until height; col <- 0 until width) { 74 | positions ++= getPosition(row, col) 75 | textCoords ++= getTexCoords(row, col) 76 | indices ++= getIndices(row, col) 77 | val currentHeight = getHeight(col, row, width, minY, maxY, heightMapImage.image) 78 | heightArray(row)(col) = currentHeight 79 | 80 | val n = if (row > 0 && row < height - 1 && col > 0 && col < width - 1) { 81 | val hL = heightArray(row - 1)(col - 0) 82 | val hR = heightArray(row + 1)(col + 0) 83 | val hD = heightArray(row - 0)(col - 1) 84 | val hU = heightArray(row + 0)(col + 1) 85 | 86 | Vector3(hL - hR, 2.0f, hD - hU).normalize 87 | } else { 88 | Vector3(0, 1, 0) 89 | } 90 | normals += n.x 91 | normals += n.y 92 | normals += n.z 93 | } 94 | ( 95 | MeshDefinition(positions.toArray, textCoords.toArray, normals.toArray, indices.toArray, materialDefinition), 96 | heightArray 97 | ) 98 | } 99 | 100 | for { 101 | meshInput <- IO.effectTotal { getMeshInput(materialDefinition) } 102 | m <- sceneShaderInterpreter.loadMesh(program, meshInput._1) 103 | } yield HeightMapMesh(minY, maxY, meshInput._2, m) 104 | } 105 | 106 | /** 107 | * Get the height from the colour of the corresponding pixel in the height map image. 108 | * 109 | * @param x x position. 110 | * @param z z position. 111 | * @param width width of height map. 112 | * @param minY min height. 113 | * @param maxY max height. 114 | * @param buffer height map. 115 | * @return height (y position). 116 | */ 117 | def getHeight(x: Int, z: Int, width: Int, minY: Float, maxY: Float, buffer: ByteBuffer) = { 118 | val r = buffer.get(x * 4 + 0 + z * 4 * width) 119 | val g = buffer.get(x * 4 + 1 + z * 4 * width) 120 | val b = buffer.get(x * 4 + 2 + z * 4 * width) 121 | // val a = buffer.get(x * 4 + 3 + z * 4 * width) 122 | val argb = 123 | // ((0xFF & a) << 24) | 124 | ((0xFF & r) << 16) | 125 | ((0xFF & g) << 8) | 126 | (0xFF & b) 127 | 128 | minY + Math.abs(maxY - minY) * (argb.toFloat / HeightMapMesh.MaxColour) 129 | } 130 | } 131 | } 132 | } 133 | 134 | final def loadHeightMap( 135 | program: SceneShaderProgram, 136 | minY: Float, 137 | maxY: Float, 138 | heightMapImage: Image, 139 | materialDefinition: MaterialDefinition, 140 | textInc: Int 141 | ): ZIO[HeightMapLoader, LoadingError, HeightMapMesh] = 142 | ZIO.accessM(_.get.load(program, minY, maxY, heightMapImage, materialDefinition, textInc)) 143 | } 144 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/loaders/particles/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.loaders 2 | 3 | import java.nio.file.Path 4 | 5 | import zio.{Has, IO, ZIO, ZLayer} 6 | import zio3d.core.math.Vector3 7 | import zio3d.engine._ 8 | import zio3d.engine.shaders.particle.{ParticleShaderInterpreter, ParticleShaderProgram} 9 | 10 | package object particles { 11 | 12 | type ParticleLoader = Has[ParticleLoader.Service] 13 | 14 | object ParticleLoader extends Serializable { 15 | trait Service { 16 | def loadGun( 17 | program: ParticleShaderProgram, 18 | modelFile: Path, 19 | meshDefinition: SimpleMeshDefinition, 20 | maxParticles: Int, 21 | firingRateMillis: Long, 22 | particleSpeed: Float, 23 | ttl: Long 24 | ): IO[LoadingError, Gun] 25 | 26 | def loadFire( 27 | program: ParticleShaderProgram, 28 | modelFile: Path, 29 | meshDefinition: SimpleMeshDefinition, 30 | maxParticles: Int, 31 | particleSpeed: Vector3, 32 | ttl: Long, 33 | creationPeriodMillis: Long, 34 | textureUpdateMillis: Long, 35 | speedRndRange: Float, 36 | positionRndRange: Float, 37 | scaleRndRange: Float, 38 | animRange: Float 39 | ): IO[LoadingError, Fires] 40 | } 41 | 42 | val live = ZLayer.fromFunction[ShaderEnv, ParticleLoader.Service] { env => 43 | new Service { 44 | 45 | private val particleShaderInterpreter = env.get[ParticleShaderInterpreter.Service] 46 | 47 | def loadGun( 48 | program: ParticleShaderProgram, 49 | modelFile: Path, 50 | meshDefinition: SimpleMeshDefinition, 51 | maxParticles: Int, 52 | firingRateMillis: Long, 53 | particleSpeed: Float, 54 | ttl: Long 55 | ) = 56 | for { 57 | mesh <- particleShaderInterpreter.loadMesh(program, meshDefinition) 58 | } yield Gun( 59 | Model.still(mesh), 60 | Particle(ItemInstance(Vector3(0f, 1f, 0f), 1.0f), Vector3.origin, ttl), 61 | List.empty, 62 | maxParticles, 63 | firingRateMillis, 64 | 0, 65 | particleSpeed 66 | ) 67 | 68 | def loadFire( 69 | program: ParticleShaderProgram, 70 | modelFile: Path, 71 | meshDefinition: SimpleMeshDefinition, 72 | maxParticles: Int, 73 | particleSpeed: Vector3, 74 | ttl: Long, 75 | creationPeriodMillis: Long, 76 | textureUpdateMillis: Long, 77 | speedRndRange: Float, 78 | positionRndRange: Float, 79 | scaleRndRange: Float, 80 | animRange: Float 81 | ) = 82 | for { 83 | mesh <- particleShaderInterpreter.loadMesh(program, meshDefinition) 84 | td = meshDefinition.material.texture 85 | texAnim = TextureAnimation(td.fold(1)(_.cols), td.fold(1)(_.rows), textureUpdateMillis) 86 | } yield Fires( 87 | Model.still(mesh), 88 | texAnim, 89 | FireSettings( 90 | maxParticles, 91 | particleSpeed, 92 | 1.0f, 93 | creationPeriodMillis, 94 | ttl, 95 | speedRndRange, 96 | positionRndRange, 97 | scaleRndRange, 98 | animRange 99 | ), 100 | List.empty 101 | ) 102 | } 103 | } 104 | } 105 | 106 | def loadGun( 107 | program: ParticleShaderProgram, 108 | modelFile: Path, 109 | meshDefinition: SimpleMeshDefinition, 110 | maxParticles: Int, 111 | firingRateMillis: Long, 112 | particleSpeed: Float, 113 | ttl: Long 114 | ): ZIO[ParticleLoader, LoadingError, Gun] = 115 | ZIO.accessM( 116 | _.get.loadGun( 117 | program, 118 | modelFile, 119 | meshDefinition, 120 | maxParticles, 121 | firingRateMillis, 122 | particleSpeed, 123 | ttl 124 | ) 125 | ) 126 | 127 | def loadFire( 128 | program: ParticleShaderProgram, 129 | modelFile: Path, 130 | meshDefinition: SimpleMeshDefinition, 131 | maxParticles: Int, 132 | particleSpeed: Vector3, 133 | ttl: Long, 134 | creationPeriodMillis: Long, 135 | textureUpdateMillis: Long, 136 | speedRndRange: Float, 137 | positionRndRange: Float, 138 | scaleRndRange: Float, 139 | animRange: Float 140 | ): ZIO[ParticleLoader, LoadingError, Fires] = 141 | ZIO.accessM( 142 | _.get.loadFire( 143 | program, 144 | modelFile, 145 | meshDefinition, 146 | maxParticles, 147 | particleSpeed, 148 | ttl, 149 | creationPeriodMillis, 150 | textureUpdateMillis, 151 | speedRndRange, 152 | positionRndRange, 153 | scaleRndRange, 154 | animRange 155 | ) 156 | ) 157 | } 158 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/loaders/terrain/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.loaders 2 | 3 | import java.nio.file.Path 4 | 5 | import zio.{Has, IO, ZIO, ZLayer} 6 | import zio3d.core.images.{Image, Images} 7 | import zio3d.core.math.Vector3 8 | import zio3d.engine._ 9 | import zio3d.engine.loaders.heightmap.HeightMapLoader 10 | import zio3d.engine.shaders.scene.SceneShaderProgram 11 | 12 | package object terrain { 13 | 14 | type TerrainLoader = Has[TerrainLoader.Service] 15 | 16 | object TerrainLoader extends Serializable { 17 | trait Service { 18 | def load( 19 | program: SceneShaderProgram, 20 | terrainSize: Int, 21 | scale: Float, 22 | minY: Float, 23 | maxY: Float, 24 | heightMap: Path, 25 | textureFile: Path, 26 | textInc: Int 27 | ): IO[LoadingError, Terrain] 28 | } 29 | 30 | val live = ZLayer.fromServices[Images.Service, HeightMapLoader.Service, TerrainLoader.Service] { (images, heightMapLoader) => 31 | new Service { 32 | 33 | override def load( 34 | program: SceneShaderProgram, 35 | terrainSize: Int, 36 | scale: Float, 37 | minY: Float, 38 | maxY: Float, 39 | heightMap: Path, 40 | textureFile: Path, 41 | textInc: Int 42 | ): ZIO[Any, LoadingError, Terrain] = 43 | for { 44 | i <- images.loadImage(heightMap, false, 4) 45 | h <- heightMapLoader.load(program, minY, maxY, i, MaterialDefinition.textured(textureFile, false), textInc) 46 | } yield generateTerrain(h, i, terrainSize, scale) 47 | 48 | private def generateTerrain(heightMapMesh: HeightMapMesh, i: Image, terrainSize: Int, scale: Float): Terrain = { 49 | val blocks = new Array[GameItem](terrainSize * terrainSize) 50 | val boundingBoxes = Array.ofDim[Box2D](terrainSize, terrainSize) 51 | for (row <- 0 until terrainSize; col <- 0 until terrainSize) { 52 | val xDisplacement = (col - (terrainSize - 1) / 2) * scale * HeightMapMesh.xLength 53 | val zDisplacement = (row - (terrainSize - 1) / 2) * scale * HeightMapMesh.zLength 54 | val inst = ItemInstance(Vector3(xDisplacement, 0, zDisplacement), scale) 55 | val block = GameItem(Model.still(heightMapMesh.mesh)) 56 | .spawn(inst) 57 | blocks(row * terrainSize + col) = block 58 | boundingBoxes(row)(col) = getBoundingBox(inst) 59 | } 60 | Terrain(blocks.toList, boundingBoxes, heightMapMesh, terrainSize, i.width - 1, i.height - 1) 61 | } 62 | 63 | private def getBoundingBox(i: ItemInstance): Box2D = { 64 | val scale = i.scale 65 | val position = i.position 66 | val topLeftX = HeightMapMesh.StartX * scale + position.x 67 | val topLeftZ = HeightMapMesh.StartZ * scale + position.z 68 | val width = Math.abs(HeightMapMesh.StartX * 2) * scale 69 | val height = Math.abs(HeightMapMesh.StartZ * 2) * scale 70 | Box2D(topLeftX, topLeftZ, width, height) 71 | } 72 | } 73 | } 74 | } 75 | 76 | def loadTerrain( 77 | program: SceneShaderProgram, 78 | terrainSize: Int, 79 | scale: Float, 80 | minY: Float, 81 | maxY: Float, 82 | heightMap: Path, 83 | textureFile: Path, 84 | textInc: Int 85 | ): ZIO[TerrainLoader, LoadingError, Terrain] = 86 | ZIO.accessM(_.get.load(program, terrainSize, scale, minY, maxY, heightMap, textureFile, textInc)) 87 | } 88 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/loaders/texture/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.loaders 2 | 3 | import java.nio.file.Path 4 | 5 | import org.lwjgl.opengl.GL11.{GL_TEXTURE_2D, GL_UNPACK_ALIGNMENT, GL_UNSIGNED_BYTE} 6 | import zio.{Has, IO, ZIO, ZLayer} 7 | import zio3d.core.CoreEnv 8 | import zio3d.core.gl.{GL, Texture} 9 | import zio3d.core.images.Images 10 | import zio3d.engine._ 11 | import zio3d.engine.loaders.LoadingError.FileLoadError 12 | 13 | package object texture { 14 | 15 | type TextureLoader = Has[TextureLoader.Service] 16 | 17 | object TextureLoader extends Serializable { 18 | trait Service { 19 | 20 | /** 21 | * Load a texture from a filename. 22 | * 23 | * @param source to load. 24 | * @param flipVertical Flips the image vertically, so the first pixel in the output array is the bottom left. 25 | * @return texture. 26 | */ 27 | def loadTexture(source: Path, flipVertical: Boolean): IO[FileLoadError, Texture] 28 | 29 | /** 30 | * Load a material from a definition. 31 | * 32 | * @param materialDefinition to load. 33 | * @return material. 34 | */ 35 | def loadMaterial(materialDefinition: MaterialDefinition): IO[FileLoadError, Material] 36 | } 37 | 38 | val live = ZLayer.fromFunction[CoreEnv, TextureLoader.Service] { env => 39 | new Service { 40 | 41 | private val gl = env.get[GL.Service] 42 | private val images = env.get[Images.Service] 43 | 44 | /** 45 | * Load a texture from a filename. 46 | * 47 | * @param source to load. 48 | * @param flipVertical Flips the image vertically, so the first pixel in the output array is the bottom left. 49 | * @return texture. 50 | */ 51 | override def loadTexture(source: Path, flipVertical: Boolean): IO[FileLoadError, Texture] = 52 | for { 53 | img <- images.loadImage(source, flipVertical, 0) 54 | 55 | t <- gl.genTextures 56 | _ <- gl.bindTexture(GL_TEXTURE_2D, t) 57 | _ <- gl.pixelStorei(GL_UNPACK_ALIGNMENT, 1) 58 | _ <- gl.texImage2D( 59 | GL_TEXTURE_2D, 60 | 0, 61 | img.format, 62 | img.width, 63 | img.height, 64 | 0, 65 | img.format, 66 | GL_UNSIGNED_BYTE, 67 | img.image 68 | ) 69 | _ <- gl.generateMipMap(GL_TEXTURE_2D) 70 | } yield t 71 | 72 | /** 73 | * Load a material from a definition. 74 | * 75 | * @param materialDefinition to load. 76 | * @return material. 77 | */ 78 | override def loadMaterial(materialDefinition: MaterialDefinition): IO[FileLoadError, Material] = 79 | materialDefinition.texture match { 80 | case None => IO.succeed(Material.empty) 81 | case Some(t) => 82 | loadTexture(t.path, t.flipVertical) map { t => 83 | Material.textured(t) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | final val textureLoader: ZIO[TextureLoader, Nothing, TextureLoader.Service] = 91 | ZIO.access(_.get) 92 | 93 | final def loadTexture(source: Path, flipVertical: Boolean): ZIO[TextureLoader, FileLoadError, Texture] = 94 | ZIO.accessM(_.get.loadTexture(source, flipVertical)) 95 | 96 | final def loadMaterial(materialDefinition: MaterialDefinition): ZIO[TextureLoader, FileLoadError, Material] = 97 | ZIO.accessM(_.get.loadMaterial(materialDefinition)) 98 | } 99 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d 2 | 3 | import zio.ZLayer 4 | import zio.blocking.Blocking 5 | import zio.clock.Clock 6 | import zio.random.Random 7 | import zio3d.core.CoreEnv 8 | import zio3d.core.assimp.Assimp 9 | import zio3d.core.images.Images 10 | import zio3d.engine.glwindow.GLWindow 11 | import zio3d.engine.loaders.assimp.anim.AnimMeshLoader 12 | import zio3d.engine.loaders.assimp.static.StaticMeshLoader 13 | import zio3d.engine.loaders.heightmap.HeightMapLoader 14 | import zio3d.engine.loaders.particles.ParticleLoader 15 | import zio3d.engine.loaders.terrain.TerrainLoader 16 | import zio3d.engine.loaders.texture.TextureLoader 17 | import zio3d.engine.shaders.particle.ParticleShaderInterpreter 18 | import zio3d.engine.shaders.scene.SceneShaderInterpreter 19 | import zio3d.engine.shaders.simple.SimpleShaderInterpreter 20 | import zio3d.engine.shaders.skybox.SkyboxShaderInterpreter 21 | 22 | package object engine { 23 | 24 | type SysEnv = Clock with Blocking with Random 25 | 26 | object SysEnv { 27 | val live: ZLayer[Any, Nothing, SysEnv] = 28 | Clock.live ++ 29 | Blocking.live ++ 30 | Random.live 31 | } 32 | 33 | type PreShaderEnv = SysEnv with CoreEnv with TextureLoader 34 | 35 | object PreShaderEnv { 36 | private val textureLoader: ZLayer[Any, Nothing, TextureLoader] = 37 | CoreEnv.live >>> TextureLoader.live 38 | 39 | val live: ZLayer[Any, Nothing, PreShaderEnv] = 40 | SysEnv.live ++ 41 | CoreEnv.live ++ 42 | textureLoader 43 | } 44 | 45 | type ShaderEnv = PreShaderEnv 46 | with SimpleShaderInterpreter 47 | with SkyboxShaderInterpreter 48 | with SceneShaderInterpreter 49 | with ParticleShaderInterpreter 50 | 51 | object ShaderEnv { 52 | val live: ZLayer[Any, Nothing, ShaderEnv] = 53 | PreShaderEnv.live ++ 54 | (PreShaderEnv.live >>> ((SimpleShaderInterpreter.live) ++ 55 | SkyboxShaderInterpreter.live ++ 56 | SceneShaderInterpreter.live ++ 57 | ParticleShaderInterpreter.live)) 58 | } 59 | 60 | type RenderEnv = ShaderEnv 61 | with TerrainLoader 62 | with StaticMeshLoader 63 | with AnimMeshLoader 64 | with ParticleLoader 65 | with GLWindow 66 | 67 | object RenderEnv { 68 | private val heightMapLoader: ZLayer[Any, Nothing, HeightMapLoader] = 69 | ShaderEnv.live >>> HeightMapLoader.live 70 | private val terrainLoader: ZLayer[Any, Nothing, TerrainLoader] = 71 | (heightMapLoader ++ Images.live) >>> TerrainLoader.live 72 | 73 | val live: ZLayer[Any, Nothing, RenderEnv] = 74 | ShaderEnv.live ++ 75 | terrainLoader ++ 76 | (Assimp.live >>> StaticMeshLoader.live) ++ 77 | (Assimp.live >>> AnimMeshLoader.live) ++ 78 | (ShaderEnv.live >>> ParticleLoader.live) ++ 79 | (CoreEnv.live >>> GLWindow.live) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/runtime/MainThreadApp.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.runtime 2 | 3 | import java.util 4 | import java.util.concurrent.{AbstractExecutorService, LinkedBlockingQueue, TimeUnit} 5 | 6 | import zio.internal.Executor 7 | import zio.{Exit, FiberFailure, IO, Runtime, ZIO, internal} 8 | 9 | import scala.concurrent.ExecutionContext 10 | 11 | /** 12 | * The entry-point for a ZIO application that must run certain effects on the main thread. 13 | */ 14 | trait MainThreadApp { 15 | private lazy val runtime = Runtime.default 16 | 17 | def run(args: List[String]): ZIO[Any, Nothing, Int] 18 | 19 | /** 20 | * The Scala main function, intended to be called only by the Scala runtime. 21 | */ 22 | final def main(args0: Array[String]): Unit = 23 | sys.exit( 24 | unsafeRunContinueMain( 25 | for { 26 | fiber <- run(args0.toList).fork 27 | _ <- IO.effectTotal(java.lang.Runtime.getRuntime.addShutdownHook(new Thread { 28 | override def run(): Unit = { 29 | val _ = runtime.unsafeRun(fiber.interrupt) 30 | } 31 | })) 32 | result <- fiber.join 33 | } yield result 34 | ).getOrElse(c => throw FiberFailure(c)) 35 | ) 36 | 37 | /** 38 | * Executes the effect while the main thread works through any work directed to it. 39 | * In particular, some GLFW methods need to be executed from the main thread. 40 | */ 41 | final def unsafeRunContinueMain[E, A](zio: ZIO[Any, E, A]): Exit[E, A] = { 42 | val result = internal.OneShot.make[Exit[E, A]] 43 | 44 | runtime.unsafeRunAsync(zio) { x: Exit[E, A] => 45 | MainThreadApp.MainExecutorService.shutdown() 46 | result.set(x) 47 | } 48 | 49 | MainThreadApp.MainExecutorService.run() 50 | result.get() 51 | } 52 | } 53 | 54 | object MainThreadApp { 55 | 56 | private[runtime] object MainExecutorService extends AbstractExecutorService { 57 | 58 | private val workQueue = new LinkedBlockingQueue[Runnable]() 59 | 60 | @volatile private var terminated = false 61 | 62 | def shutdown(): Unit = 63 | terminated = true 64 | 65 | def isShutdown: Boolean = 66 | terminated 67 | 68 | def isTerminated: Boolean = 69 | terminated 70 | 71 | @throws[InterruptedException] 72 | def awaitTermination(theTimeout: Long, theUnit: TimeUnit): Boolean = { 73 | shutdown() 74 | terminated 75 | } 76 | 77 | def shutdownNow: util.List[Runnable] = new util.ArrayList[Runnable]() 78 | 79 | def execute(theCommand: Runnable): Unit = 80 | workQueue.put(theCommand) 81 | 82 | /** 83 | * Must be called synchronously from the main thread. 84 | */ 85 | private[runtime] def run(): Unit = 86 | while (!terminated) { 87 | val work = workQueue.poll(1, TimeUnit.MILLISECONDS) 88 | if (work != null) { 89 | work.run() 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * The main thread is exposed here as an Executor to be used via [[ZIO.lock()]]. 96 | */ 97 | val mainThread: Executor = 98 | Executor.fromExecutionContext(10)(ExecutionContext.fromExecutorService(MainExecutorService)) 99 | } 100 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/shaders/ShaderInterpreter.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.shaders 2 | 3 | import zio.{IO, UIO} 4 | import zio3d.engine._ 5 | import zio3d.engine.loaders.LoadingError 6 | 7 | /** 8 | * ShaderInterpreter contains the logic for a single shader program, typically consisting 9 | * of a vertex shader and a fragment shader. 10 | * @tparam M mesh type. 11 | * @tparam P shader program. 12 | */ 13 | trait ShaderInterpreter[M, P] { 14 | def shaderService: ShaderInterpreter.Service[M, P] 15 | } 16 | 17 | object ShaderInterpreter { 18 | 19 | /** 20 | * @tparam M mesh input. 21 | * @tparam P shader program. 22 | */ 23 | trait Service[M, P] { 24 | 25 | /** 26 | * Create a new shader program, compiling the required shaders and linking to the program. 27 | * @return program. 28 | */ 29 | def loadShaderProgram: IO[LoadingError, P] 30 | 31 | /** 32 | * Use the shader program defined by this pipeline to load a mesh for the given mesh definition/input. 33 | * @param program shader program. 34 | * @param input mesh definition. 35 | * @return mesh. 36 | */ 37 | def loadMesh(program: P, input: M): IO[LoadingError, Mesh] 38 | 39 | /** 40 | * Use the shader program defined by this pipeline to render the given items. 41 | * 42 | * @param program shader program. 43 | * @param item to render. 44 | * @param transformation to apply. 45 | * @param fixtures environment fixtures (light, fog, etc). 46 | * @return . 47 | */ 48 | def render( 49 | program: P, 50 | item: GameItem, 51 | transformation: Transformation, 52 | fixtures: Fixtures 53 | ): UIO[Unit] 54 | 55 | def cleanup( 56 | program: P 57 | ): UIO[Unit] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/shaders/particle/ParticleShaderProgram.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.shaders.particle 2 | 3 | import zio3d.core.gl.{AttribLocation, Program, UniformLocation} 4 | 5 | final case class ParticleShaderProgram( 6 | program: Program, 7 | uniModelViewMatrix: UniformLocation, 8 | uniProjectionMatrix: UniformLocation, 9 | uniTextureSampler: UniformLocation, 10 | uniTexXOffset: UniformLocation, 11 | uniTexYOffset: UniformLocation, 12 | uniNumCols: UniformLocation, 13 | uniNumRows: UniformLocation, 14 | positionAttr: AttribLocation, 15 | texCoordAttr: AttribLocation, 16 | normalsAttr: AttribLocation 17 | ) 18 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/shaders/particle/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.shaders 2 | 3 | import org.lwjgl.opengl.GL11._ 4 | import org.lwjgl.opengl.GL13.GL_TEXTURE0 5 | import org.lwjgl.opengl.GL15.{GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_STATIC_COPY, GL_STATIC_DRAW} 6 | import org.lwjgl.opengl.GL20 7 | import zio._ 8 | import zio3d.core.buffers.Buffers 9 | import zio3d.core.gl._ 10 | import zio3d.engine._ 11 | import zio3d.engine.loaders.LoadingError 12 | import zio3d.engine.loaders.LoadingError.{ProgramLinkError, ShaderCompileError} 13 | import zio3d.engine.loaders.texture.TextureLoader 14 | 15 | package object particle { 16 | 17 | type ParticleShaderInterpreter = Has[ParticleShaderInterpreter.Service] 18 | 19 | object ParticleShaderInterpreter { 20 | 21 | trait Service extends ShaderInterpreter.Service[SimpleMeshDefinition, ParticleShaderProgram] 22 | 23 | val live = ZLayer.fromFunction[PreShaderEnv, ParticleShaderInterpreter.Service] { env => 24 | new Service { 25 | 26 | private val gl = env.get[GL.Service] 27 | private val buffers = env.get[Buffers.Service] 28 | private val textureLoader = env.get[TextureLoader.Service] 29 | 30 | private val strVertexShader = 31 | """#version 330 32 | | 33 | |layout (location=0) in vec3 position; 34 | |layout (location=1) in vec2 texCoord; 35 | |layout (location=2) in vec3 vertexNormal; 36 | | 37 | |out vec2 outTexCoord; 38 | | 39 | |uniform mat4 modelViewMatrix; 40 | |uniform mat4 projectionMatrix; 41 | | 42 | |uniform float texXOffset; 43 | |uniform float texYOffset; 44 | |uniform int numCols; 45 | |uniform int numRows; 46 | | 47 | |void main() 48 | |{ 49 | | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 50 | | 51 | | // Support for texture atlas, update texture coordinates 52 | | float x = (texCoord.x / numCols + texXOffset); 53 | | float y = (texCoord.y / numRows + texYOffset); 54 | | 55 | | outTexCoord = vec2(x, y); 56 | |} 57 | """.stripMargin 58 | 59 | private val strFragmentShader = 60 | """#version 330 61 | | 62 | |in vec2 outTexCoord; 63 | |out vec4 fragColor; 64 | | 65 | |uniform sampler2D textureSampler; 66 | | 67 | |void main() 68 | |{ 69 | | fragColor = texture(textureSampler, outTexCoord); 70 | |} 71 | """.stripMargin 72 | 73 | def loadShaderProgram: IO[LoadingError, ParticleShaderProgram] = 74 | for { 75 | vs <- gl.compileShader(GL20.GL_VERTEX_SHADER, strVertexShader).mapError(ShaderCompileError) 76 | fs <- gl.compileShader(GL20.GL_FRAGMENT_SHADER, strFragmentShader).mapError(ShaderCompileError) 77 | p <- gl.createProgram 78 | _ <- gl.attachShader(p, vs) 79 | _ <- gl.attachShader(p, fs) 80 | _ <- gl.linkProgram(p) 81 | _ <- gl.useProgram(p) 82 | ls <- gl.getProgrami(p, GL20.GL_LINK_STATUS) 83 | _ <- gl.getProgramInfoLog(p).flatMap(log => IO.fail(ProgramLinkError(log))).when(ls == 0) 84 | 85 | uniProjectionMatrix <- gl.getUniformLocation(p, "projectionMatrix") 86 | uniModelViewMatrix <- gl.getUniformLocation(p, "modelViewMatrix") 87 | uniTexSampler <- gl.getUniformLocation(p, "textureSampler") 88 | uniTexXOffset <- gl.getUniformLocation(p, "texXOffset") 89 | uniTexYOffset <- gl.getUniformLocation(p, "texYOffset") 90 | uniNumCols <- gl.getUniformLocation(p, "numCols") 91 | uniNumRows <- gl.getUniformLocation(p, "numRows") 92 | 93 | positionAttr <- gl.getAttribLocation(p, "position") 94 | texCoordAttr <- gl.getAttribLocation(p, "texCoord") 95 | normalAttr <- gl.getAttribLocation(p, "vertexNormal") 96 | } yield ParticleShaderProgram( 97 | p, 98 | uniModelViewMatrix, 99 | uniProjectionMatrix, 100 | uniTexSampler, 101 | uniTexXOffset, 102 | uniTexYOffset, 103 | uniNumCols, 104 | uniNumRows, 105 | positionAttr, 106 | texCoordAttr, 107 | normalAttr 108 | ) 109 | 110 | override def loadMesh(program: ParticleShaderProgram, input: SimpleMeshDefinition) = 111 | for { 112 | vao <- gl.genVertexArrays() 113 | _ <- gl.bindVertexArray(vao) 114 | 115 | posVbo <- gl.genBuffers 116 | posBuf <- buffers.floatBuffer(input.positions) 117 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, posVbo) 118 | _ <- gl.bufferData(GL_ARRAY_BUFFER, posBuf, GL_STATIC_DRAW) 119 | _ <- gl.vertexAttribPointer(program.positionAttr, 3, GL_FLOAT, false, 0, 0) 120 | 121 | texVbo <- gl.genBuffers 122 | texBuf <- buffers.floatBuffer(input.texCoords) 123 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, texVbo) 124 | _ <- gl.bufferData(GL_ARRAY_BUFFER, texBuf, GL_STATIC_DRAW) 125 | _ <- gl.vertexAttribPointer(program.texCoordAttr, 2, GL_FLOAT, false, 0, 0) 126 | 127 | norVbo <- gl.genBuffers 128 | norBuf <- buffers.floatBuffer(input.normals) 129 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, norVbo) 130 | _ <- gl.bufferData(GL_ARRAY_BUFFER, norBuf, GL_STATIC_DRAW) 131 | _ <- gl.vertexAttribPointer(program.normalsAttr, 3, GL_FLOAT, false, 0, 0) 132 | 133 | indVbo <- gl.genBuffers 134 | indBuf <- buffers.intBuffer(input.indices) 135 | _ <- gl.bindBuffer(GL_ELEMENT_ARRAY_BUFFER, indVbo) 136 | _ <- gl.bufferData(GL_ELEMENT_ARRAY_BUFFER, indBuf, GL_STATIC_COPY) 137 | 138 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, VertexBufferObject.None) 139 | _ <- gl.bindVertexArray(VertexArrayObject.None) 140 | 141 | t <- textureLoader.loadMaterial(input.material) 142 | } yield Mesh(vao, List(posVbo, texVbo, indVbo), input.indices.length, t) 143 | 144 | override def render( 145 | program: ParticleShaderProgram, 146 | item: GameItem, 147 | transformation: Transformation, 148 | fixtures: Fixtures 149 | ) = 150 | gl.useProgram(program.program) *> 151 | gl.uniformMatrix4fv(program.uniProjectionMatrix, false, transformation.projectionMatrix) *> 152 | gl.uniform1i(program.uniTextureSampler, 0) *> 153 | gl.depthMask(false) *> 154 | gl.blendFunc(GL_SRC_ALPHA, GL_ONE) *> 155 | ZIO.foreach(item.model.meshes)(m => renderMesh(program, m, item.instances, transformation)) *> 156 | gl.blendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) *> 157 | gl.depthMask(true) *> 158 | gl.useProgram(Program.None) 159 | 160 | private def renderMesh( 161 | program: ParticleShaderProgram, 162 | mesh: Mesh, 163 | items: List[ItemInstance], 164 | trans: Transformation 165 | ) = 166 | mesh.material.texture.fold(IO.unit)(bindTexture) *> 167 | gl.bindVertexArray(mesh.vao) *> 168 | gl.enableVertexAttribArray(program.positionAttr) *> 169 | gl.enableVertexAttribArray(program.texCoordAttr) *> 170 | gl.enableVertexAttribArray(program.normalsAttr) *> 171 | ZIO.foreach(items)(i => renderInstance(program, mesh, i, trans)) *> 172 | gl.disableVertexAttribArray(program.positionAttr) *> 173 | gl.disableVertexAttribArray(program.texCoordAttr) *> 174 | gl.disableVertexAttribArray(program.normalsAttr) *> 175 | gl.bindTexture(GL_TEXTURE_2D, Texture.None) *> 176 | gl.bindVertexArray(VertexArrayObject.None) 177 | 178 | private def renderInstance( 179 | program: ParticleShaderProgram, 180 | mesh: Mesh, 181 | item: ItemInstance, 182 | transformation: Transformation 183 | ) = { 184 | val textureAnim = item.textureAnimation 185 | val numCols = textureAnim.fold(1)(_.cols) 186 | val numRows = textureAnim.fold(1)(_.rows) 187 | val textPos = textureAnim.fold(0)(_.currentFrame) 188 | val col = textPos % numCols 189 | val row = textPos / numCols 190 | val texXOffset = col.toFloat / numCols 191 | val texYOffset = row.toFloat / numRows 192 | 193 | gl.uniform1i(program.uniNumCols, numCols) *> 194 | gl.uniform1i(program.uniNumRows, numRows) *> 195 | gl.uniform1f(program.uniTexXOffset, texXOffset) *> 196 | gl.uniform1f(program.uniTexYOffset, texYOffset) *> 197 | gl.uniformMatrix4fv(program.uniModelViewMatrix, false, buildModelViewMatrix(transformation, item)) *> 198 | gl.drawElements(GL_TRIANGLES, mesh.vertexCount, GL_UNSIGNED_INT, 0) 199 | } 200 | 201 | private def buildModelViewMatrix(t: Transformation, i: ItemInstance) = { 202 | val modelMatrix = t.getModelMatrix(i) 203 | t.viewMatrix * t.viewMatrix.transpose3x3(modelMatrix) 204 | } 205 | 206 | private def bindTexture(texture: Texture) = 207 | gl.activeTexture(GL_TEXTURE0) *> 208 | gl.bindTexture(GL_TEXTURE_2D, texture) 209 | 210 | override def cleanup(program: ParticleShaderProgram): UIO[Unit] = 211 | gl.deleteProgram(program.program) 212 | } 213 | } 214 | } 215 | 216 | final def loadShaderProgram: ZIO[ParticleShaderInterpreter, LoadingError, ParticleShaderProgram] = 217 | ZIO.accessM(_.get.loadShaderProgram) 218 | 219 | final def render( 220 | program: ParticleShaderProgram, 221 | item: GameItem, 222 | transformation: Transformation, 223 | fixtures: Fixtures 224 | ): ZIO[ParticleShaderInterpreter, Nothing, Unit] = 225 | ZIO.accessM(_.get.render(program, item, transformation, fixtures)) 226 | 227 | final def loadMesh( 228 | program: ParticleShaderProgram, 229 | input: SimpleMeshDefinition 230 | ): ZIO[ParticleShaderInterpreter, LoadingError, Mesh] = 231 | ZIO.accessM(_.get.loadMesh(program, input)) 232 | 233 | final def cleanup(program: ParticleShaderProgram): ZIO[ParticleShaderInterpreter, Nothing, Unit] = 234 | ZIO.accessM(_.get.cleanup(program)) 235 | } 236 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/shaders/scene/types.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.shaders.scene 2 | 3 | import zio3d.core.gl.{AttribLocation, Program, UniformLocation} 4 | 5 | final case class SceneShaderProgram( 6 | program: Program, 7 | uniModelViewMatrix: UniformLocation, 8 | uniProjectionMatrix: UniformLocation, 9 | uniJointsMatrix: UniformLocation, 10 | uniTextureSampler: UniformLocation, 11 | uniMaterial: MaterialUniform, 12 | uniSpecularPower: UniformLocation, 13 | uniAmbientLight: UniformLocation, 14 | uniPointLight: List[PointLightUniform], 15 | uniSpotLight: List[SpotLightUniform], 16 | uniFog: FogUniform, 17 | positionAttr: AttribLocation, 18 | texCoordAttr: AttribLocation, 19 | normalsAttr: AttribLocation, 20 | jointWeightsAttr: AttribLocation, 21 | jointIndicesAttr: AttribLocation 22 | ) 23 | 24 | final case class MaterialUniform( 25 | ambient: UniformLocation, 26 | diffuse: UniformLocation, 27 | specular: UniformLocation, 28 | hasTexture: UniformLocation, 29 | reflectance: UniformLocation 30 | ) 31 | 32 | final case class PointLightUniform( 33 | colour: UniformLocation, 34 | position: UniformLocation, 35 | intensity: UniformLocation, 36 | attConstant: UniformLocation, 37 | attLinear: UniformLocation, 38 | attExponent: UniformLocation 39 | ) 40 | 41 | final case class SpotLightUniform( 42 | pl: PointLightUniform, 43 | conedir: UniformLocation, 44 | cutoff: UniformLocation 45 | ) 46 | 47 | final case class FogUniform( 48 | active: UniformLocation, 49 | colour: UniformLocation, 50 | density: UniformLocation 51 | ) 52 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/shaders/simple/SimpleShaderProgram.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.shaders.simple 2 | 3 | import zio3d.core.gl.{AttribLocation, Program, UniformLocation} 4 | 5 | final case class SimpleShaderProgram( 6 | program: Program, 7 | uniModelViewMatrix: UniformLocation, 8 | uniProjectionMatrix: UniformLocation, 9 | uniTextureSampler: UniformLocation, 10 | positionAttr: AttribLocation, 11 | texCoordAttr: AttribLocation, 12 | normalsAttr: AttribLocation 13 | ) 14 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/shaders/simple/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.shaders 2 | 3 | import org.lwjgl.opengl.GL11.{GL_FLOAT, GL_TEXTURE_2D, GL_TRIANGLES, GL_UNSIGNED_INT} 4 | import org.lwjgl.opengl.GL13.GL_TEXTURE0 5 | import org.lwjgl.opengl.GL15.{GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_STATIC_COPY, GL_STATIC_DRAW} 6 | import org.lwjgl.opengl.GL20 7 | import zio.{Has, IO, ZIO, ZLayer} 8 | import zio3d.core.buffers.Buffers 9 | import zio3d.core.gl.{GL, Program, Texture, VertexArrayObject, VertexBufferObject} 10 | import zio3d.engine._ 11 | import zio3d.engine.loaders.LoadingError 12 | import zio3d.engine.loaders.LoadingError.{ProgramLinkError, ShaderCompileError} 13 | import zio3d.engine.loaders.texture.TextureLoader 14 | 15 | package object simple { 16 | 17 | type SimpleShaderInterpreter = Has[SimpleShaderInterpreter.Service] 18 | 19 | object SimpleShaderInterpreter extends Serializable { 20 | 21 | trait Service extends ShaderInterpreter.Service[SimpleMeshDefinition, SimpleShaderProgram] 22 | 23 | val live = ZLayer.fromFunction[PreShaderEnv, SimpleShaderInterpreter.Service] { env => 24 | new Service { 25 | 26 | private val gl = env.get[GL.Service] 27 | private val buffers = env.get[Buffers.Service] 28 | private val textureLoader = env.get[TextureLoader.Service] 29 | 30 | private val strVertexShader = 31 | """#version 330 32 | | 33 | |layout (location=0) in vec3 position; 34 | |layout (location=1) in vec2 texCoord; 35 | |layout (location=2) in vec3 vertexNormal; 36 | | 37 | |out vec2 outTexCoord; 38 | | 39 | |uniform mat4 modelViewMatrix; 40 | |uniform mat4 projectionMatrix; 41 | | 42 | |void main() 43 | |{ 44 | | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 45 | | outTexCoord = texCoord; 46 | |} 47 | """.stripMargin 48 | 49 | private val strFragmentShader = 50 | """#version 330 51 | | 52 | |in vec2 outTexCoord; 53 | |out vec4 fragColor; 54 | | 55 | |uniform sampler2D textureSampler; 56 | | 57 | |void main() 58 | |{ 59 | | fragColor = texture(textureSampler, outTexCoord); 60 | |} 61 | """.stripMargin 62 | 63 | def loadShaderProgram = 64 | for { 65 | vs <- gl.compileShader(GL20.GL_VERTEX_SHADER, strVertexShader).mapError(ShaderCompileError) 66 | fs <- gl.compileShader(GL20.GL_FRAGMENT_SHADER, strFragmentShader).mapError(ShaderCompileError) 67 | p <- gl.createProgram 68 | _ <- gl.attachShader(p, vs) 69 | _ <- gl.attachShader(p, fs) 70 | _ <- gl.linkProgram(p) 71 | _ <- gl.useProgram(p) 72 | ls <- gl.getProgrami(p, GL20.GL_LINK_STATUS) 73 | _ <- gl.getProgramInfoLog(p).flatMap(log => IO.fail(ProgramLinkError(log))).when(ls == 0) 74 | 75 | uniProjectionMatrix <- gl.getUniformLocation(p, "projectionMatrix") 76 | uniModelViewMatrix <- gl.getUniformLocation(p, "modelViewMatrix") 77 | uniTexSampler <- gl.getUniformLocation(p, "textureSampler") 78 | 79 | positionAttr <- gl.getAttribLocation(p, "position") 80 | texCoordAttr <- gl.getAttribLocation(p, "texCoord") 81 | normalAttr <- gl.getAttribLocation(p, "vertexNormal") 82 | 83 | } yield SimpleShaderProgram( 84 | p, 85 | uniModelViewMatrix, 86 | uniProjectionMatrix, 87 | uniTexSampler, 88 | positionAttr, 89 | texCoordAttr, 90 | normalAttr 91 | ) 92 | 93 | def loadMesh(program: SimpleShaderProgram, input: SimpleMeshDefinition) = 94 | for { 95 | vao <- gl.genVertexArrays() 96 | _ <- gl.bindVertexArray(vao) 97 | 98 | posVbo <- gl.genBuffers 99 | posBuf <- buffers.floatBuffer(input.positions) 100 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, posVbo) 101 | _ <- gl.bufferData(GL_ARRAY_BUFFER, posBuf, GL_STATIC_DRAW) 102 | _ <- gl.vertexAttribPointer(program.positionAttr, 3, GL_FLOAT, false, 0, 0) 103 | 104 | texVbo <- gl.genBuffers 105 | texBuf <- buffers.floatBuffer(input.texCoords) 106 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, texVbo) 107 | _ <- gl.bufferData(GL_ARRAY_BUFFER, texBuf, GL_STATIC_DRAW) 108 | _ <- gl.vertexAttribPointer(program.texCoordAttr, 2, GL_FLOAT, false, 0, 0) 109 | 110 | norVbo <- gl.genBuffers 111 | norBuf <- buffers.floatBuffer(input.normals) 112 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, norVbo) 113 | _ <- gl.bufferData(GL_ARRAY_BUFFER, norBuf, GL_STATIC_DRAW) 114 | _ <- gl.vertexAttribPointer(program.normalsAttr, 3, GL_FLOAT, false, 0, 0) 115 | 116 | indVbo <- gl.genBuffers 117 | indBuf <- buffers.intBuffer(input.indices) 118 | _ <- gl.bindBuffer(GL_ELEMENT_ARRAY_BUFFER, indVbo) 119 | _ <- gl.bufferData(GL_ELEMENT_ARRAY_BUFFER, indBuf, GL_STATIC_COPY) 120 | 121 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, VertexBufferObject.None) 122 | _ <- gl.bindVertexArray(VertexArrayObject.None) 123 | 124 | t <- textureLoader.loadMaterial(input.material) 125 | } yield Mesh(vao, List(posVbo, texVbo, indVbo), input.indices.length, t) 126 | 127 | def render( 128 | program: SimpleShaderProgram, 129 | item: GameItem, 130 | transformation: Transformation, 131 | fixtures: Fixtures 132 | ) = 133 | gl.useProgram(program.program) *> 134 | gl.uniformMatrix4fv(program.uniProjectionMatrix, false, transformation.projectionMatrix) *> 135 | gl.uniform1i(program.uniTextureSampler, 0) *> 136 | ZIO.foreach(item.model.meshes)(m => renderMesh(program, m, item.instances, transformation)) *> 137 | gl.useProgram(Program.None) 138 | 139 | private def renderMesh( 140 | program: SimpleShaderProgram, 141 | mesh: Mesh, 142 | items: List[ItemInstance], 143 | trans: Transformation 144 | ) = 145 | mesh.material.texture.fold(IO.unit)(bindTexture) *> 146 | gl.bindVertexArray(mesh.vao) *> 147 | gl.enableVertexAttribArray(program.positionAttr) *> 148 | gl.enableVertexAttribArray(program.texCoordAttr) *> 149 | gl.enableVertexAttribArray(program.normalsAttr) *> 150 | ZIO.foreach(items)(i => renderInstance(program, mesh, i, trans)) *> 151 | gl.disableVertexAttribArray(program.positionAttr) *> 152 | gl.disableVertexAttribArray(program.texCoordAttr) *> 153 | gl.disableVertexAttribArray(program.normalsAttr) *> 154 | gl.bindTexture(GL_TEXTURE_2D, Texture.None) *> 155 | gl.bindVertexArray(VertexArrayObject.None) 156 | 157 | private def renderInstance( 158 | program: SimpleShaderProgram, 159 | mesh: Mesh, 160 | item: ItemInstance, 161 | transformation: Transformation 162 | ) = 163 | gl.uniformMatrix4fv(program.uniModelViewMatrix, false, transformation.getModelViewMatrix(item)) *> 164 | gl.drawElements(GL_TRIANGLES, mesh.vertexCount, GL_UNSIGNED_INT, 0) 165 | 166 | private def bindTexture(texture: Texture) = 167 | gl.activeTexture(GL_TEXTURE0) *> 168 | gl.bindTexture(GL_TEXTURE_2D, texture) 169 | 170 | def cleanup(program: SimpleShaderProgram) = 171 | gl.deleteProgram(program.program) 172 | } 173 | } 174 | } 175 | 176 | final def loadShaderProgram: ZIO[SimpleShaderInterpreter, LoadingError, SimpleShaderProgram] = 177 | ZIO.accessM(_.get.loadShaderProgram) 178 | 179 | final def loadMesh( 180 | program: SimpleShaderProgram, 181 | input: SimpleMeshDefinition 182 | ): ZIO[SimpleShaderInterpreter, LoadingError, Mesh] = 183 | ZIO.accessM(_.get.loadMesh(program, input)) 184 | 185 | final def render( 186 | program: SimpleShaderProgram, 187 | item: GameItem, 188 | transformation: Transformation, 189 | fixtures: Fixtures 190 | ): ZIO[SimpleShaderInterpreter, Nothing, Unit] = 191 | ZIO.accessM(_.get.render(program, item, transformation, fixtures)) 192 | 193 | final def cleanup(program: SimpleShaderProgram): ZIO[SimpleShaderInterpreter, Nothing, Unit] = 194 | ZIO.accessM(_.get.cleanup(program)) 195 | } 196 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/shaders/skybox/SkyboxShaderProgram.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.shaders.skybox 2 | 3 | import zio3d.core.gl.{AttribLocation, Program, UniformLocation} 4 | 5 | final case class SkyboxShaderProgram( 6 | program: Program, 7 | uniModelViewMatrix: UniformLocation, 8 | uniProjectionMatrix: UniformLocation, 9 | uniSkybox: UniformLocation, 10 | uniAmbientLight: UniformLocation, 11 | positionAttr: AttribLocation, 12 | texCoordAttr: AttribLocation 13 | ) 14 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/engine/shaders/skybox/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.engine.shaders 2 | 3 | import java.nio.file.Path 4 | 5 | import org.lwjgl.opengl.GL11._ 6 | import org.lwjgl.opengl.GL12.{GL_CLAMP_TO_EDGE, GL_TEXTURE_WRAP_R} 7 | import org.lwjgl.opengl.GL13.{GL_TEXTURE0, GL_TEXTURE_CUBE_MAP, GL_TEXTURE_CUBE_MAP_POSITIVE_X} 8 | import org.lwjgl.opengl.GL15.{GL_ARRAY_BUFFER, GL_STATIC_DRAW} 9 | import org.lwjgl.opengl.GL20 10 | import zio._ 11 | import zio3d.core.buffers.Buffers 12 | import zio3d.core.gl._ 13 | import zio3d.core.images.Images 14 | import zio3d.engine._ 15 | import zio3d.engine.loaders.LoadingError 16 | import zio3d.engine.loaders.LoadingError.{ProgramLinkError, ShaderCompileError} 17 | 18 | package object skybox { 19 | type SkyboxShaderInterpreter = Has[SkyboxShaderInterpreter.Service] 20 | 21 | object SkyboxShaderInterpreter { 22 | 23 | trait Service extends ShaderInterpreter.Service[SkyboxDefinition, SkyboxShaderProgram] 24 | 25 | val live = ZLayer.fromFunction[PreShaderEnv, SkyboxShaderInterpreter.Service] { env => 26 | new Service { 27 | 28 | private val gl = env.get[GL.Service] 29 | private val buffers = env.get[Buffers.Service] 30 | private val images = env.get[Images.Service] 31 | 32 | private val strVertexShader = 33 | """#version 330 34 | | 35 | |layout (location=0) in vec3 position; 36 | | 37 | |out vec3 texCoord; 38 | | 39 | |uniform mat4 modelViewMatrix; 40 | |uniform mat4 projectionMatrix; 41 | | 42 | |void main() 43 | |{ 44 | | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 45 | | texCoord = position; 46 | |} 47 | """.stripMargin 48 | 49 | private val strFragmentShader = 50 | s"""#version 330 51 | |out vec4 fragColor; 52 | | 53 | |in vec3 texCoord; 54 | | 55 | |uniform samplerCube skybox; 56 | |uniform vec3 ambientLight; 57 | | 58 | |void main() 59 | |{ 60 | | fragColor = vec4(ambientLight, 1) * texture(skybox, texCoord); 61 | |} 62 | """.stripMargin 63 | 64 | def loadShaderProgram = 65 | for { 66 | vs <- gl.compileShader(GL20.GL_VERTEX_SHADER, strVertexShader).mapError(ShaderCompileError) 67 | fs <- gl.compileShader(GL20.GL_FRAGMENT_SHADER, strFragmentShader).mapError(ShaderCompileError) 68 | p <- gl.createProgram 69 | _ <- gl.attachShader(p, vs) 70 | _ <- gl.attachShader(p, fs) 71 | _ <- gl.linkProgram(p) 72 | _ <- gl.useProgram(p) 73 | ls <- gl.getProgrami(p, GL20.GL_LINK_STATUS) 74 | _ <- gl.getProgramInfoLog(p).flatMap(log => IO.fail(ProgramLinkError(log))).when(ls == 0) 75 | 76 | uniProjectionMatrix <- gl.getUniformLocation(p, "projectionMatrix") 77 | uniModelViewMatrix <- gl.getUniformLocation(p, "modelViewMatrix") 78 | uniSkybox <- gl.getUniformLocation(p, "skybox") 79 | uniAmbientLight <- gl.getUniformLocation(p, "ambientLight") 80 | 81 | positionAttr <- gl.getAttribLocation(p, "position") 82 | texCoordAttr <- gl.getAttribLocation(p, "texCoord") 83 | 84 | } yield SkyboxShaderProgram( 85 | p, 86 | uniModelViewMatrix, 87 | uniProjectionMatrix, 88 | uniSkybox, 89 | uniAmbientLight, 90 | positionAttr, 91 | texCoordAttr 92 | ) 93 | 94 | def loadMesh(program: SkyboxShaderProgram, input: SkyboxDefinition) = 95 | for { 96 | vao <- gl.genVertexArrays() 97 | _ <- gl.bindVertexArray(vao) 98 | 99 | vbo <- gl.genBuffers 100 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, vbo) 101 | buf <- buffers.floatBuffer(input.vertices) 102 | _ <- gl.bufferData(GL_ARRAY_BUFFER, buf, GL_STATIC_DRAW) 103 | _ <- gl.vertexAttribPointer(program.positionAttr, 3, GL_FLOAT, false, 0, 0) 104 | 105 | _ <- gl.bindBuffer(GL_ARRAY_BUFFER, VertexBufferObject.None) 106 | _ <- gl.bindVertexArray(VertexArrayObject.None) 107 | 108 | t <- loadTexture(input.textures) 109 | 110 | } yield Mesh(vao, List(vbo), 12, Material.textured(t)) 111 | 112 | def loadTexture(faces: List[Path]) = 113 | for { 114 | t <- gl.genTextures 115 | _ <- gl.bindTexture(GL_TEXTURE_CUBE_MAP, t) 116 | _ <- IO.foreach(faces.zipWithIndex)(f => loadCubeFace(f._1, f._2)) 117 | _ <- gl.texParameter(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR) 118 | _ <- gl.texParameter(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR) 119 | _ <- gl.texParameter(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) 120 | _ <- gl.texParameter(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) 121 | _ <- gl.texParameter(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) 122 | } yield t 123 | 124 | private def loadCubeFace(source: Path, i: Int) = 125 | for { 126 | img <- images.loadImage(source, flipVertical = false, 3) 127 | _ <- gl.texImage2D( 128 | GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 129 | 0, 130 | img.format, 131 | img.width, 132 | img.height, 133 | 0, 134 | img.format, 135 | GL_UNSIGNED_BYTE, 136 | img.image 137 | ) 138 | } yield () 139 | 140 | def render( 141 | program: SkyboxShaderProgram, 142 | item: GameItem, 143 | transformation: Transformation, 144 | fixtures: Fixtures 145 | ) = 146 | gl.useProgram(program.program) *> 147 | gl.uniform1i(program.uniSkybox, 0) *> 148 | gl.uniformMatrix4fv(program.uniProjectionMatrix, false, transformation.projectionMatrix) *> 149 | gl.uniform3f(program.uniAmbientLight, 1.0f, 1.0f, 1.0f) *> 150 | ZIO.foreach(item.model.meshes)(m => renderMesh(program, m, item.instances, transformation)) *> 151 | gl.bindTexture(GL_TEXTURE_CUBE_MAP, Texture.None) *> 152 | gl.bindVertexArray(VertexArrayObject.None) *> 153 | gl.useProgram(Program.None) 154 | 155 | private def renderMesh( 156 | program: SkyboxShaderProgram, 157 | mesh: Mesh, 158 | items: List[ItemInstance], 159 | trans: Transformation 160 | ): UIO[Unit] = 161 | gl.depthFunc(GL_LEQUAL) *> 162 | gl.bindVertexArray(mesh.vao) *> 163 | mesh.material.texture.fold(IO.unit)(t => bindTexture(t)) *> 164 | gl.enableVertexAttribArray(program.positionAttr) *> 165 | gl.enableVertexAttribArray(program.texCoordAttr) *> 166 | ZIO.foreach(items)(i => renderInstance(program, i, trans)) *> 167 | gl.disableVertexAttribArray(program.positionAttr) *> 168 | gl.disableVertexAttribArray(program.texCoordAttr) *> 169 | gl.bindVertexArray(VertexArrayObject.None) *> 170 | gl.depthFunc(GL_LESS) 171 | 172 | private def renderInstance( 173 | program: SkyboxShaderProgram, 174 | i: ItemInstance, 175 | transformation: Transformation 176 | ) = 177 | gl.uniformMatrix4fv(program.uniModelViewMatrix, false, transformation.noTranslation.getModelViewMatrix(i)) *> 178 | gl.drawArrays(GL_TRIANGLES, 0, 36) 179 | 180 | private def bindTexture(texture: Texture) = 181 | gl.activeTexture(GL_TEXTURE0) *> 182 | gl.bindTexture(GL_TEXTURE_CUBE_MAP, texture) 183 | 184 | override def cleanup(program: SkyboxShaderProgram) = 185 | gl.deleteProgram(program.program) 186 | } 187 | } 188 | } 189 | 190 | final def loadShaderProgram: ZIO[SkyboxShaderInterpreter, LoadingError, SkyboxShaderProgram] = 191 | ZIO.accessM(_.get.loadShaderProgram) 192 | 193 | final def loadMesh( 194 | program: SkyboxShaderProgram, 195 | input: SkyboxDefinition 196 | ): ZIO[SkyboxShaderInterpreter, LoadingError, Mesh] = 197 | ZIO.accessM(_.get.loadMesh(program, input)) 198 | 199 | final def render( 200 | program: SkyboxShaderProgram, 201 | item: GameItem, 202 | transformation: Transformation, 203 | fixtures: Fixtures 204 | ): ZIO[SkyboxShaderInterpreter, Nothing, Unit] = 205 | ZIO.accessM(_.get.render(program, item, transformation, fixtures)) 206 | 207 | final def cleanup(program: SkyboxShaderProgram): ZIO[SkyboxShaderInterpreter, Nothing, Unit] = 208 | ZIO.accessM(_.get.cleanup(program)) 209 | } 210 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/game/Game.scala: -------------------------------------------------------------------------------- 1 | package zio3d.game 2 | 3 | import zio.ZIO 4 | import zio3d.core.glfw.WindowSize 5 | import zio3d.engine.UserInput 6 | import zio3d.engine.loaders.LoadingError 7 | 8 | /** 9 | * Template for a 3D rendered application or game. 10 | * 11 | * @tparam R renderer. 12 | * @tparam S state. 13 | */ 14 | trait Game[R, S] { 15 | 16 | type Continue = Boolean 17 | 18 | /** 19 | * Initialise the renderer. The renderer comprises all shaders used for loading and rendering models 20 | * in the app. 21 | * @return renderer. 22 | */ 23 | def initRenderer: ZIO[GameEnv, LoadingError, R] 24 | 25 | /** 26 | * Create the initial app state. 27 | * Models, textures and other items that are part of the app state can be loaded here. 28 | * @param renderer may be used for loading. 29 | * @return initial state. 30 | */ 31 | def initialState(renderer: R): ZIO[GameEnv, LoadingError, S] 32 | 33 | /** 34 | * Render the current game state into a single frame. 35 | * @param windowSize window size. 36 | * @param renderer renderer. 37 | * @param state current state, to render. 38 | */ 39 | def render( 40 | windowSize: WindowSize, 41 | renderer: R, 42 | state: S 43 | ): ZIO[GameEnv, Nothing, Unit] 44 | 45 | /** 46 | * Advance the game state. 47 | * @param state current state, to render. 48 | * @param userInput user input. 49 | * @param currentTime millis since epoch. 50 | * @return next state, and whether to continue the application. 51 | */ 52 | def nextState( 53 | state: S, 54 | userInput: UserInput, 55 | currentTime: Long 56 | ): ZIO[GameEnv, Nothing, (S, Continue)] 57 | 58 | def cleanup(renderer: R, state: S): ZIO[GameEnv, Nothing, Unit] 59 | } 60 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/game/GameResources.scala: -------------------------------------------------------------------------------- 1 | package zio3d.game 2 | 3 | import java.nio.file.{Path, Paths} 4 | 5 | object GameResources { 6 | 7 | object models { 8 | final val boblamp = unsafePath("/models/bob/boblamp.md5mesh") 9 | 10 | final val house = unsafePath("/models/house/house.obj") 11 | 12 | final val monster = unsafePath("/models/monster/monster.md5mesh") 13 | 14 | final val particle = unsafePath("/models/particle/particle.obj") 15 | 16 | final val tree = unsafePath("/models/tree/palm_tree.FBX") 17 | 18 | final val cybertruck = unsafePath("/models/cybertruck/tesla_ct_export1123.fbx") 19 | } 20 | 21 | object textures { 22 | final val heightmap = unsafePath("/textures/heightmap.png") 23 | 24 | final val terrain = unsafePath("/textures/terrain.png") 25 | 26 | object fire { 27 | final val image = unsafePath("/models/particle/particle_anim.png") 28 | final val cols = 4 29 | final val rows = 4 30 | } 31 | 32 | final val bullet = unsafePath("/models/particle/particle.png") 33 | 34 | object skybox { 35 | final val front = unsafePath("/models/skybox/drakeq_ft.tga") 36 | final val back = unsafePath("/models/skybox/drakeq_bk.tga") 37 | final val up = unsafePath("/models/skybox/drakeq_up.tga") 38 | final val down = unsafePath("/models/skybox/drakeq_dn.tga") 39 | final val right = unsafePath("/models/skybox/drakeq_rt.tga") 40 | final val left = unsafePath("/models/skybox/drakeq_lf.tga") 41 | } 42 | } 43 | 44 | object fonts { 45 | final val regular = unsafePath("/fonts/OpenSans-Regular.ttf") 46 | 47 | final val bold = unsafePath("/fonts/OpenSans-Bold.ttf") 48 | } 49 | 50 | def unsafePath(name: String): Path = 51 | path(name).getOrElse(throw new RuntimeException(s"Resource not found: $name")) 52 | 53 | // note: as assimp is a C++ library, must provide path to raw file on disk, not resource in jar 54 | def path(name: String): Option[Path] = 55 | Option(Paths.get("./src/main/resources", name)) 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/game/Main.scala: -------------------------------------------------------------------------------- 1 | package zio3d.game 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import zio.blocking.blocking 6 | import zio.clock.currentTime 7 | import zio.{ZIO, ZLayer} 8 | import zio3d.core.glfw.{Window, WindowSize} 9 | import zio3d.core.{CoreEnv, gl} 10 | import zio3d.engine._ 11 | import zio3d.engine.loaders.LoadingError 12 | import zio3d.engine.runtime.MainThreadApp 13 | import zio3d.engine.runtime.MainThreadApp.mainThread 14 | import zio3d.game.hud.HudRenderer 15 | 16 | object Main extends MainThreadApp { 17 | 18 | val gameEnv: ZLayer[Any, Nothing, GameEnv] = 19 | RenderEnv.live ++ 20 | (CoreEnv.live >>> HudRenderer.live) 21 | 22 | def run(args: List[String]) = 23 | Runner.runGame(Zio3dGame).provideLayer(gameEnv).either 24 | .map(_.fold(err => { println(s"Error: $err"); 1 }, _ => 0)) 25 | } 26 | 27 | object Runner { 28 | val title = "Zio3D" 29 | val windowWidth = 400 30 | val windowHeight = 400 31 | 32 | def runGame[R, S](game: Game[R, S]): ZIO[GameEnv, LoadingError, Unit] = 33 | glwindow 34 | .open(title, windowWidth, windowHeight) 35 | .lock(mainThread) 36 | .bracket( 37 | w => glwindow.close(w).lock(mainThread), 38 | window => 39 | for { 40 | c <- game.initRenderer.lock(mainThread) 41 | s <- game.initialState(c).lock(mainThread) 42 | now <- currentTime(TimeUnit.MILLISECONDS) 43 | _ <- gameLoop(window, game, c, s, None, now) 44 | _ <- game.cleanup(c, s).lock(mainThread) 45 | } yield () 46 | ) 47 | 48 | import zio3d.core.glfw 49 | 50 | private def gameLoop[R, S]( 51 | window: Window, 52 | game: Game[R, S], 53 | r: R, 54 | st: S, 55 | i: Option[UserInput], 56 | t: Long 57 | ): ZIO[GameEnv, Nothing, Unit] = 58 | for { 59 | _ <- glfw.pollEvents.lock(mainThread) 60 | s <- glfw.getWindowSize(window).lock(mainThread) 61 | i <- glwindow.getUserInput(window, i) 62 | _ <- blocking(render(window, game, s, r, st)) 63 | t <- currentTime(TimeUnit.MILLISECONDS) 64 | n <- game.nextState(st, i, t) 65 | _ <- glfw.swapBuffers(window).lock(mainThread) 66 | x <- glfw.windowShouldClose(window).lock(mainThread) 67 | _ <- gameLoop(window, game, r, n._1, Some(i), t).when(n._2 && !x) 68 | } yield () 69 | 70 | private def render[R, S]( 71 | window: Window, 72 | game: Game[R, S], 73 | windowSize: WindowSize, 74 | renderer: R, 75 | state: S 76 | ) = 77 | for { 78 | _ <- glfw.makeContextCurrent(window) 79 | _ <- gl.createCapabilities 80 | n <- game.render(windowSize, renderer, state) 81 | } yield n 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/game/Zio3dGame.scala: -------------------------------------------------------------------------------- 1 | package zio3d.game 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import org.lwjgl.opengl.GL11 6 | import zio.ZIO 7 | import zio.clock._ 8 | import zio.random._ 9 | import zio3d.core.gl 10 | import zio3d.core.glfw.WindowSize 11 | import zio3d.core.math._ 12 | import zio3d.engine._ 13 | import zio3d.engine.loaders.assimp.anim.loadAnimMesh 14 | import zio3d.engine.loaders.assimp.static.loadStaticMesh 15 | import zio3d.engine.loaders.particles.{loadFire, loadGun} 16 | import zio3d.engine.loaders.terrain.loadTerrain 17 | import zio3d.engine.shaders.particle.ParticleShaderProgram 18 | import zio3d.engine.shaders.scene.SceneShaderProgram 19 | import zio3d.engine.shaders.simple.SimpleShaderProgram 20 | import zio3d.engine.shaders.skybox.SkyboxShaderProgram 21 | import zio3d.game.GameResources.{fonts, models, textures} 22 | import zio3d.game.config._ 23 | import zio3d.game.hud.{HudContext, HudState} 24 | 25 | final case class Zio3dRenderer( 26 | simpleShaderProgram: SimpleShaderProgram, 27 | skyboxShaderProgram: SkyboxShaderProgram, 28 | sceneShaderProgram: SceneShaderProgram, 29 | particleShaderProgram: ParticleShaderProgram, 30 | hudContext: HudContext, 31 | perspective: Perspective 32 | ) 33 | 34 | object Zio3dGame extends Game[Zio3dRenderer, Zio3dState] { 35 | 36 | final val config = GameConfig.live 37 | final val level = config.level 38 | 39 | override def initRenderer = 40 | for { 41 | _ <- gl.createCapabilities 42 | _ <- gl.enable(GL11.GL_DEPTH_TEST) 43 | m <- shaders.simple.loadShaderProgram 44 | s <- shaders.skybox.loadShaderProgram 45 | sc <- shaders.scene.loadShaderProgram 46 | p <- shaders.particle.loadShaderProgram 47 | h <- hud.init(fonts.bold) 48 | } yield Zio3dRenderer(m, s, sc, p, h, config.perspective) 49 | 50 | override def initialState(r: Zio3dRenderer) = 51 | for { 52 | terrain <- loadTerrain( 53 | r.sceneShaderProgram, 54 | level.terrain.size, 55 | level.terrain.scale, 56 | level.terrain.minY, 57 | level.terrain.maxY, 58 | level.terrain.heightMap, 59 | level.terrain.textureFile, 60 | level.terrain.textInc 61 | ) 62 | skybox <- loadSkybox(r, level.sky) 63 | staticObjs <- loadStaticObjects(r, level.staticObjects) 64 | monsters <- loadMonsters(r, level.monsters, terrain) 65 | initPosition = terrain.getPosition(level.startPosition).getOrElse(Vector3.origin) 66 | 67 | fire <- loadFire( 68 | r.particleShaderProgram, 69 | models.particle, 70 | SimpleMeshDefinition.animatedImage2D(textures.fire.image, false, textures.fire.cols, textures.fire.rows), 71 | config.fire.maxParticles, 72 | config.fire.particleSpeed, 73 | config.fire.particleTtl, 74 | config.fire.particleCreationPeriodMillis, 75 | config.fire.particleUpdateTextureMillis, 76 | config.fire.randomRange, 77 | config.fire.randomRange, 78 | config.fire.randomRange, 79 | config.fire.animRange 80 | ) 81 | gun <- loadGun( 82 | r.particleShaderProgram, 83 | models.particle, 84 | SimpleMeshDefinition.image2D(textures.bullet, false), 85 | config.gun.maxBullets, 86 | config.gun.firingRateMillis, 87 | config.gun.bulletSpeed, 88 | config.gun.bulletTtl 89 | ) 90 | 91 | now <- currentTime(TimeUnit.MILLISECONDS) 92 | } yield Zio3dState( 93 | now, 94 | terrain, 95 | skybox, 96 | monsters, 97 | List.empty, 98 | staticObjs, 99 | fire, 100 | gun, 101 | Camera(initPosition, level.startFacing, 0), 102 | level.ambientLight, 103 | flashLight = makeSpotlight(initPosition), 104 | level.fog, 105 | HudState.initial(now) 106 | ) 107 | 108 | private def makeSpotlight(initPosition: Vector3) = 109 | SpotLight( 110 | PointLight(Vector3(1, 1, 1), initPosition, 0.9f, Attenuation(0.0f, 0.0f, 0.01f)), 111 | Vector3(0, -1, 0), 112 | cos(toRadians(60.0f)) 113 | ) 114 | 115 | private def loadSkybox(r: Zio3dRenderer, sky: SkyboxDefinition) = 116 | shaders.skybox 117 | .loadMesh(r.skyboxShaderProgram, sky) 118 | .map(Model.still) 119 | .map(GameItem(_).spawn(ItemInstance(Vector3.origin, sky.scale))) 120 | 121 | private def loadStaticObjects(r: Zio3dRenderer, staticObjects: List[GameObject]) = 122 | ZIO 123 | .foreach(staticObjects) { o => 124 | for { 125 | i <- loadStaticMesh(o.model) 126 | m <- shaders.scene.loadMesh(r.sceneShaderProgram, i.head) 127 | } yield o.instances.map { i => 128 | GameItem(Model.still(m)) 129 | .spawn( 130 | ItemInstance( 131 | Vector3(i.position.x, 0, i.position.y), 132 | o.scale, 133 | o.rotation * AxisAngle4.y(i.orientation).quaternion 134 | ) 135 | ) 136 | } 137 | } 138 | .map(_.flatten) 139 | 140 | private def loadMonsters(r: Zio3dRenderer, monsters: List[GameObject], terrain: Terrain) = 141 | ZIO 142 | .foreach(monsters) { o => 143 | for { 144 | a <- loadAnimMesh(o.model) 145 | m <- ZIO.foreach(a.meshes)(m => shaders.scene.loadMesh(r.sceneShaderProgram, m)) 146 | i <- spawnInstances(o, m, a.animations, terrain) 147 | } yield a.animations.headOption.fold(GameItem(Model.still(m), i))(a => GameItem(Model.animated(m, a), i)) 148 | } 149 | 150 | private def spawnInstances(obj: GameObject, meshes: List[Mesh], animations: List[Animation], terrain: Terrain) = { 151 | val numFrames = animations.headOption.map(_.frames.length) 152 | ZIO 153 | .foreach(obj.instances) { i => 154 | nextInt.map { rand => 155 | terrain.getPosition(i.position) map { pos => 156 | ItemInstance( 157 | pos, 158 | obj.scale, 159 | obj.rotation * AxisAngle4.y(i.orientation).quaternion, 160 | obj.boxSize, 161 | numFrames.map(f => ModelAnimation(f, currentFrame = abs(rand) % f)), 162 | None 163 | ) 164 | } 165 | } 166 | } 167 | .map(_.flatten) 168 | } 169 | 170 | override def render(windowSize: WindowSize, r: Zio3dRenderer, s: Zio3dState) = { 171 | val t = r.perspective.getTransformation(windowSize, s.camera) 172 | 173 | gl.clear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT) *> 174 | gl.clearColor(0.0f, 0.0f, 0.0f, 0.0f) *> 175 | ZIO.foreach(s.simpleItems)(i => shaders.simple.render(r.simpleShaderProgram, i, t, s.fixtures)) *> 176 | ZIO.foreach(s.sceneItems)(i => shaders.scene.render(r.sceneShaderProgram, i, t, s.fixtures)) *> 177 | ZIO.foreach(s.skyboxItems)(i => shaders.skybox.render(r.skyboxShaderProgram, i, t, s.fixtures)) *> 178 | ZIO.foreach(s.particles)(i => shaders.particle.render(r.particleShaderProgram, i, t, s.fixtures)) *> 179 | hud.render(r.hudContext, windowSize, s.hud) *> 180 | gl.enable(GL11.GL_DEPTH_TEST) *> 181 | gl.enable(GL11.GL_STENCIL_TEST) 182 | } 183 | 184 | override def nextState(s: Zio3dState, input: UserInput, currentTime: Long) = 185 | s.nextState(input, currentTime) map { n => 186 | (n, !input.keys.contains(Key.ESC)) 187 | } 188 | 189 | override def cleanup(r: Zio3dRenderer, s: Zio3dState) = 190 | cleanupState(s) *> 191 | shaders.simple.cleanup(r.simpleShaderProgram) *> 192 | shaders.scene.cleanup(r.sceneShaderProgram) *> 193 | shaders.skybox.cleanup(r.skyboxShaderProgram) *> 194 | shaders.particle.cleanup(r.particleShaderProgram) 195 | 196 | private def cleanupState(s: Zio3dState) = 197 | ZIO.foreach(s.simpleItems)(cleanupItem) *> 198 | ZIO.foreach(s.sceneItems)(cleanupItem) *> 199 | ZIO.foreach(s.skyboxItems)(cleanupItem) *> 200 | ZIO.foreach(s.particles)(cleanupItem) 201 | 202 | private def cleanupItem(i: GameItem) = 203 | ZIO.foreach(i.model.meshes) { m => 204 | ZIO.foreach(m.material.texture)(gl.deleteTextures) *> 205 | ZIO.foreach(m.vbos)(gl.deleteBuffers) *> 206 | gl.deleteVertexArrays(m.vao) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/game/Zio3dState.scala: -------------------------------------------------------------------------------- 1 | package zio3d.game 2 | 3 | import zio.ZIO 4 | import zio.random.Random 5 | import zio3d.core.math.Vector3 6 | import zio3d.engine._ 7 | import zio3d.game.hud.HudState 8 | 9 | import scala.annotation.tailrec 10 | 11 | trait Zio3dScene { 12 | 13 | def skyboxItems: List[GameItem] 14 | 15 | def sceneItems: List[GameItem] 16 | 17 | def simpleItems: List[GameItem] 18 | 19 | def particles: List[GameItem] 20 | 21 | def fixtures: Fixtures 22 | } 23 | 24 | final case class Zio3dState( 25 | time: Long, 26 | terrain: Terrain, 27 | skybox: GameItem, 28 | monsters: List[GameItem], 29 | simpleObjects: List[GameItem], 30 | sceneObjects: List[GameItem], 31 | fires: Fires, 32 | gun: Gun, 33 | camera: Camera, 34 | ambientLight: Vector3, 35 | flashLight: SpotLight, 36 | fog: Fog, 37 | hud: HudState 38 | ) extends Zio3dScene { 39 | 40 | // per millisecond 41 | final val moveSpeed = 0.005f 42 | final val cameraHeight = 1.8f 43 | final val mouseSensitivity = 5.0f 44 | 45 | override def skyboxItems = List(skybox) 46 | 47 | override def sceneItems = monsters ++ sceneObjects ++ terrain.blocks 48 | 49 | override def simpleItems = simpleObjects 50 | 51 | override def particles = GameItem(gun.bulletModel, gun.renderItems) :: fires.gameItem :: Nil 52 | 53 | override def fixtures = Fixtures(LightSources(ambientLight, None, Nil, List(flashLight)), fog) 54 | 55 | def nextState(userInput: UserInput, currentTime: Long): ZIO[Random, Nothing, Zio3dState] = { 56 | val elapsedMillis = currentTime - time 57 | val (survivingMonsters, destroyedBullets, newFires) = handleBulletCollisions(currentTime, monsters, fires) 58 | 59 | for { 60 | f <- fires.update(currentTime, elapsedMillis) 61 | g = if (userInput.mouseButtons.contains(MouseButton.BUTTON_LEFT)) 62 | gun 63 | .copy(particles = gun.particles.diff(destroyedBullets)) 64 | .update(elapsedMillis) 65 | .fire(currentTime, camera.position, camera.front) 66 | else gun.copy(particles = gun.particles.diff(destroyedBullets)).update(elapsedMillis) 67 | c = nextCamera(userInput, elapsedMillis) 68 | l = flashLight.withDirection(c.front).withPosition(c.position) 69 | } yield copy( 70 | time = currentTime, 71 | monsters = survivingMonsters.map(_.animate), 72 | fires = f ++ newFires, 73 | gun = g, 74 | camera = c, 75 | flashLight = l, 76 | hud = hud.incFrames(currentTime) 77 | ) 78 | } 79 | 80 | private def handleBulletCollisions( 81 | time: Long, 82 | monsters: List[GameItem], 83 | fires: Fires 84 | ): (List[GameItem], List[Particle], Fires) = 85 | monsters.foldLeft((List.empty[GameItem], List.empty[Particle], fires.copy(fires = Nil))) { (acc, m) => 86 | val (survivors, destroyedBullets, newFires) = 87 | handleBulletCollisions(time, m.instances, Nil, Nil, fires.copy(fires = Nil)) 88 | (GameItem(m.model, survivors) :: acc._1, destroyedBullets ++ acc._2, newFires ++ acc._3) 89 | } 90 | 91 | @tailrec 92 | private def handleBulletCollisions( 93 | time: Long, 94 | mons: List[ItemInstance], 95 | survivingMons: List[ItemInstance], 96 | destroyedBullets: List[Particle], 97 | newFires: Fires 98 | ): (List[ItemInstance], List[Particle], Fires) = 99 | mons match { 100 | case Nil => (survivingMons, destroyedBullets, newFires) 101 | case m :: ms => 102 | gun.particles.find(p => m.aabbContains(p.item.position)) match { 103 | case Some(p) => 104 | handleBulletCollisions( 105 | time, 106 | ms, 107 | survivingMons, 108 | p :: destroyedBullets, 109 | newFires.startFire(time, p.item.position) 110 | ) 111 | case None => 112 | handleBulletCollisions(time, ms, m :: survivingMons, destroyedBullets, newFires) 113 | } 114 | } 115 | 116 | def nextCamera(userInput: UserInput, elapsedMillis: Long): Camera = { 117 | val keys = userInput.keys 118 | val cursorMovement = userInput.cursorMovement 119 | 120 | // move forward/back 121 | val dz = 122 | if (keys.contains(Key.UP)) moveSpeed * elapsedMillis 123 | else if (keys.contains(Key.DOWN)) -moveSpeed * elapsedMillis 124 | else 0f 125 | 126 | // strafe 127 | val dx = 128 | if (keys.contains(Key.LEFT)) -moveSpeed * elapsedMillis 129 | else if (keys.contains(Key.RIGHT)) moveSpeed * elapsedMillis 130 | else 0f 131 | 132 | // still need to look up terrain for y-position... 133 | val nextCameraProvisional = camera 134 | .movePosition(dx, 0, dz) 135 | 136 | terrain 137 | .getHeight(nextCameraProvisional.position) 138 | .fold(camera)(y => nextCameraProvisional.withHeight(y + cameraHeight)) 139 | .rotate(yawDelta = cursorMovement.x / mouseSensitivity, pitchDelta = -cursorMovement.y / mouseSensitivity) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/game/config/GameConfig.scala: -------------------------------------------------------------------------------- 1 | package zio3d.game.config 2 | 3 | import java.nio.file.Path 4 | 5 | import zio3d.core.math._ 6 | import zio3d.engine.{Fog, Perspective, SkyboxDefinition} 7 | import zio3d.game.GameResources.{models, textures} 8 | 9 | final case class GunConfig( 10 | maxBullets: Int, 11 | firingRateMillis: Long, 12 | bulletSpeed: Float, 13 | bulletTtl: Long 14 | ) 15 | 16 | final case class FireConfig( 17 | randomRange: Float, 18 | maxParticles: Int, 19 | particleTtl: Long, 20 | particleSpeed: Vector3, 21 | particleCreationPeriodMillis: Long, 22 | particleUpdateTextureMillis: Long, 23 | animRange: Float 24 | ) 25 | 26 | final case class GameConfig( 27 | perspective: Perspective, 28 | gun: GunConfig, 29 | fire: FireConfig, 30 | level: GameLevel 31 | ) 32 | 33 | object GameConfig { 34 | val perspective = Perspective( 35 | fov = toRadians(60.0f), 36 | zNear = 0.01f, 37 | zFar = 1000.0f 38 | ) 39 | 40 | val gun = GunConfig( 41 | maxBullets = 100, 42 | firingRateMillis = 100, 43 | bulletSpeed = 20.0f, 44 | bulletTtl = 1000 45 | ) 46 | 47 | val fire = FireConfig( 48 | randomRange = 0.3f, 49 | maxParticles = 100, 50 | particleTtl = 1500, 51 | particleSpeed = Vector3(0, 1, 0) * 0.4f, 52 | particleCreationPeriodMillis = 300, 53 | particleUpdateTextureMillis = 200, 54 | animRange = 10 55 | ) 56 | 57 | val live = GameConfig( 58 | perspective = perspective, 59 | gun = gun, 60 | fire = fire, 61 | GameLevel.level1 62 | ) 63 | } 64 | 65 | final case class Instance( 66 | position: Vector2, 67 | orientation: Float = 0f 68 | ) 69 | 70 | final case class GameObject( 71 | model: Path, 72 | scale: Float, 73 | boxSize: Float, 74 | instances: List[Instance], 75 | rotation: Quaternion = Quaternion.Zero 76 | ) 77 | 78 | final case class TerrainDefinition( 79 | size: Int, 80 | scale: Float, 81 | minY: Float, 82 | maxY: Float, 83 | heightMap: Path, 84 | textureFile: Path, 85 | textInc: Int 86 | ) 87 | 88 | final case class GameLevel( 89 | terrain: TerrainDefinition, 90 | sky: SkyboxDefinition, 91 | fog: Fog, 92 | ambientLight: Vector3, 93 | staticObjects: List[GameObject], 94 | monsters: List[GameObject], 95 | startPosition: Vector2, 96 | startFacing: Float 97 | ) 98 | 99 | object GameLevel { 100 | val sky = SkyboxDefinition( 101 | textures.skybox.front, 102 | textures.skybox.back, 103 | textures.skybox.up, 104 | textures.skybox.down, 105 | textures.skybox.left, 106 | textures.skybox.right, 107 | 50f 108 | ) 109 | 110 | val terrain = TerrainDefinition( 111 | 3, 112 | 20, 113 | -0.1f, 114 | 0.1f, 115 | textures.heightmap, 116 | textures.terrain, 117 | 40 118 | ) 119 | 120 | val fog = Fog(active = true, Vector3(0.5f, 0.5f, 0.5f), 0.05f) 121 | 122 | val level1 = GameLevel( 123 | terrain = terrain, 124 | sky = sky, 125 | fog = fog, 126 | ambientLight = Vector3(0.5f, 0.5f, 0.5f), 127 | staticObjects = List( 128 | GameObject( 129 | models.house, 130 | scale = 0.5f, 131 | boxSize = 5f, 132 | instances = List( 133 | Instance(Vector2(0f, 15f)) 134 | ) 135 | ), 136 | GameObject( 137 | models.tree, 138 | scale = 0.02f, 139 | rotation = AxisAngle4.x(toRadians(-90)).quaternion, 140 | boxSize = 1f, 141 | instances = List( 142 | Instance(Vector2(10f, 10f)), 143 | Instance(Vector2(-10f, 15f)), 144 | Instance(Vector2(-11f, 5f)), 145 | Instance(Vector2(-20f, 14f)), 146 | Instance(Vector2(-13f, 11f)) 147 | ) 148 | ) 149 | ), 150 | monsters = List( 151 | GameObject( 152 | models.boblamp, 153 | scale = 0.02f, 154 | boxSize = 1.0f, 155 | instances = List( 156 | Instance(Vector2(-2f, 15f)) 157 | ) 158 | ), 159 | GameObject( 160 | models.monster, 161 | scale = 0.02f, 162 | boxSize = 1.5f, 163 | instances = 164 | List( 165 | Instance(Vector2(-2f, -8f), toRadians(-90f)), 166 | Instance(Vector2(2f, -8f), toRadians(-90f)) 167 | ) ++ 168 | createArmyPositions(Vector2(0, -25f), 25, 8, 1.5f).map(p => Instance(p, toRadians(-90))).toList 169 | ), 170 | GameObject( 171 | models.cybertruck, 172 | rotation = AxisAngle4.x(toRadians(-90)).quaternion, 173 | scale = 1.0f, 174 | boxSize = 2.0f, 175 | instances = List( 176 | Instance(Vector2(12f, -2f)) 177 | ) 178 | ) 179 | ), 180 | startPosition = Vector2.origin, 181 | startFacing = -90 182 | ) 183 | 184 | def createArmyPositions(pos: Vector2, rows: Int, cols: Int, spacing: Float) = { 185 | val x = pos.x - (rows * spacing / 2) 186 | val y = pos.y - (cols * spacing / 2) 187 | for { 188 | r <- 0 until rows 189 | c <- 0 until cols 190 | } yield Vector2(x + r * spacing, y + c * spacing) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/game/hud/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d.game 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.channels.ByteChannel 5 | import java.nio.file.{Files, Path} 6 | 7 | import org.lwjgl.nanovg.NVGColor 8 | import org.lwjgl.nanovg.NanoVG.{NVG_ALIGN_CENTER, NVG_ALIGN_TOP} 9 | import org.lwjgl.nanovg.NanoVGGL3.NVG_STENCIL_STROKES 10 | import zio._ 11 | import zio3d.core.CoreEnv 12 | import zio3d.core.buffers.Buffers 13 | import zio3d.core.glfw.WindowSize 14 | import zio3d.core.nvg.NVG 15 | import zio3d.engine.loaders.LoadingError 16 | 17 | package object hud { 18 | 19 | type HudRenderer = Has[HudRenderer.Service] 20 | 21 | object HudRenderer extends Serializable { 22 | trait Service { 23 | def init(font: Path): IO[LoadingError, HudContext] 24 | 25 | def render(context: HudContext, windowSize: WindowSize, state: HudState): UIO[Unit] 26 | } 27 | 28 | val live = ZLayer.fromFunction[CoreEnv, HudRenderer.Service] { env => 29 | new Service { 30 | 31 | private val nvg = env.get[NVG.Service] 32 | private val buffers = env.get[Buffers.Service] 33 | 34 | val FontName = "BOLD" 35 | 36 | def init(font: Path): IO[LoadingError, HudContext] = 37 | for { 38 | n <- nvg.create(NVG_STENCIL_STROKES) 39 | f <- toByteBuffer(font).mapError(th => LoadingError.FileLoadError(font.toString, th.getMessage)) 40 | ok <- nvg.createFontMem(n, FontName, f, 0) 41 | _ <- IO.fail(LoadingError.FileLoadError(font.toString, "Unable to load font")).when(ok != 0) 42 | } yield HudContext(n, NVGColor.create(), f) 43 | 44 | def render(hud: HudContext, windowSize: WindowSize, state: HudState): UIO[Unit] = 45 | for { 46 | _ <- nvg.beginFrame(hud.vg, windowSize.width, windowSize.height, 1) 47 | 48 | // circle 49 | _ <- nvg.beginPath(hud.vg) 50 | _ <- nvg.circle(hud.vg, windowSize.width.toFloat / 2, windowSize.height.toFloat / 2, 20.0f) 51 | _ <- nvg.fillColor(hud.vg, rgba(0xff, 0xff, 0xff, 50, hud.color)) 52 | _ <- nvg.fill(hud.vg) 53 | 54 | _ <- nvg.fontSize(hud.vg, 25.0f) 55 | _ <- nvg.fontFace(hud.vg, FontName) 56 | _ <- nvg.textAlign(hud.vg, NVG_ALIGN_CENTER | NVG_ALIGN_TOP) 57 | _ <- nvg.fillColor(hud.vg, rgba(0xff, 0xff, 0xff, 255, hud.color)) 58 | _ <- nvg.text(hud.vg, windowSize.width.toFloat - 150, windowSize.height.toFloat - 75, s"${state.fps} fps") 59 | 60 | _ <- nvg.endFrame(hud.vg) 61 | } yield () 62 | 63 | private def rgba(r: Int, g: Int, b: Int, a: Int, color: NVGColor) = { 64 | color.r(r / 255.0f) 65 | color.g(g / 255.0f) 66 | color.b(b / 255.0f) 67 | color.a(a / 255.0f) 68 | color 69 | } 70 | 71 | private def toByteBuffer(resource: Path): Task[ByteBuffer] = 72 | for { 73 | fc <- Task.effect { Files.newByteChannel(resource) } 74 | buf <- buffers.byteBuffer(fc.size().toInt + 1) 75 | res <- read(fc, buf) 76 | } yield res 77 | 78 | private def read(fc: ByteChannel, buf: ByteBuffer): Task[ByteBuffer] = 79 | Task.effect(fc.read(buf)).flatMap { r => 80 | if (r == -1) { 81 | Task.effect(buf.flip().asInstanceOf[ByteBuffer]) 82 | } else { 83 | read(fc, buf) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | final val hudRenderer: ZIO[HudRenderer, Nothing, HudRenderer.Service] = 90 | ZIO.access(_.get) 91 | 92 | final def init(font: Path): ZIO[HudRenderer, LoadingError, HudContext] = 93 | ZIO.accessM(_.get.init(font)) 94 | 95 | def render(context: HudContext, windowSize: WindowSize, state: HudState): ZIO[HudRenderer, Nothing, Unit] = 96 | ZIO.accessM(_.get.render(context, windowSize, state)) 97 | } 98 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/game/hud/types.scala: -------------------------------------------------------------------------------- 1 | package zio3d.game.hud 2 | 3 | import java.nio.ByteBuffer 4 | 5 | import org.lwjgl.nanovg.NVGColor 6 | import zio3d.core.nvg.NVGContext 7 | 8 | final case class HudContext( 9 | vg: NVGContext, 10 | color: NVGColor, 11 | fontBuffer: ByteBuffer // pin the buffer here so it doesn't get GC'ed 12 | ) 13 | 14 | final case class HudState( 15 | frameStartMillis: Long, 16 | frameCount: Int, 17 | fps: Int 18 | ) { 19 | 20 | def incFrames(now: Long): HudState = 21 | if (now - frameStartMillis > 1000) { 22 | HudState(now, 0, frameCount) 23 | } else { 24 | copy(frameCount = frameCount + 1) 25 | } 26 | } 27 | 28 | object HudState { 29 | def initial(startMillis: Long): HudState = 30 | HudState(startMillis, 0, 0) 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/zio3d/game/package.scala: -------------------------------------------------------------------------------- 1 | package zio3d 2 | 3 | import zio3d.engine.RenderEnv 4 | import zio3d.game.hud.HudRenderer 5 | 6 | package object game { 7 | type GameEnv = RenderEnv with HudRenderer 8 | } 9 | --------------------------------------------------------------------------------