├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── orthographic │ ├── orthographic.collection │ └── orthographic.script └── perspective │ ├── perspective.atlas │ ├── perspective.collection │ └── perspective.script ├── game.project └── rendy ├── rendy.camera ├── rendy.go ├── rendy.lua ├── rendy.render ├── rendy.render_script └── rendy.script /.gitattributes: -------------------------------------------------------------------------------- 1 | # Defold Protocol Buffer Text Files (https://github.com/github/linguist/issues/5091) 2 | *.animationset linguist-language=JSON5 3 | *.atlas linguist-language=JSON5 4 | *.camera linguist-language=JSON5 5 | *.collection linguist-language=JSON5 6 | *.collectionfactory linguist-language=JSON5 7 | *.collectionproxy linguist-language=JSON5 8 | *.collisionobject linguist-language=JSON5 9 | *.cubemap linguist-language=JSON5 10 | *.display_profiles linguist-language=JSON5 11 | *.factory linguist-language=JSON5 12 | *.font linguist-language=JSON5 13 | *.gamepads linguist-language=JSON5 14 | *.go linguist-language=JSON5 15 | *.gui linguist-language=JSON5 16 | *.input_binding linguist-language=JSON5 17 | *.label linguist-language=JSON5 18 | *.material linguist-language=JSON5 19 | *.mesh linguist-language=JSON5 20 | *.model linguist-language=JSON5 21 | *.particlefx linguist-language=JSON5 22 | *.render linguist-language=JSON5 23 | *.sound linguist-language=JSON5 24 | *.sprite linguist-language=JSON5 25 | *.spinemodel linguist-language=JSON5 26 | *.spinescene linguist-language=JSON5 27 | *.texture_profiles linguist-language=JSON5 28 | *.tilemap linguist-language=JSON5 29 | *.tilesource linguist-language=JSON5 30 | 31 | # Defold JSON Files 32 | *.buffer linguist-language=JSON 33 | 34 | # Defold GLSL Shaders 35 | *.fp linguist-language=GLSL 36 | *.vp linguist-language=GLSL 37 | 38 | # Defold Lua Files 39 | *.editor_script linguist-language=Lua 40 | *.render_script linguist-language=Lua 41 | *.script linguist-language=Lua 42 | *.gui_script linguist-language=Lua 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.internal 2 | /build 3 | .externalToolBuilders 4 | .DS_Store 5 | Thumbs.db 6 | .lock-wscript 7 | *.pyc 8 | .project 9 | .cproject 10 | builtins -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 White Box Dev 2 | 3 | This software is provided 'as-is', without any express or implied warranty. 4 | In no event will the authors be held liable for any damages arising from the use of this software. 5 | 6 | Permission is granted to anyone to use this software for any purpose, 7 | including commercial applications, and to alter it and redistribute it freely, 8 | subject to the following restrictions: 9 | 10 | 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. 11 | If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 12 | 13 | 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 14 | 15 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Defold Rendy 2 | 3 | Defold Rendy provides a versatile camera suite and render pipeline in a Defold game engine project. 4 | 5 | Please click the ☆ button on GitHub if this repository is useful or interesting. Thank you! 6 | 7 | ![thumbnail](https://github.com/user-attachments/assets/b35a1c1d-9c6b-43da-9ff4-64538955070f) 8 | 9 | ## Installation 10 | 11 | Add the latest version to your project's dependencies: 12 | https://github.com/whiteboxdev/library-defold-rendy/archive/main.zip 13 | 14 | ## Tutorial 15 | 16 | ### Initialization (1 of 7) 17 | 18 | In the *game.project* file's Bootstrap section, set the active Render component to *rendy.render* file: 19 | 20 | ![bootstrap](https://github.com/user-attachments/assets/254ee7b3-10ba-473c-8e0b-69054a93089a) 21 | 22 | Rendy provides a pre-packaged *rendy.go* game object that contains a camera component and a script component, which communicate with the *rendy.lua* file and the *rendy.render_script* file. Multiple cameras may be active simultaneously, all with their own projection, viewports, and other properties. 23 | 24 | --- 25 | 26 | ### Configuration (2 of 7) 27 | 28 | Let's take a look at the camera's default configuration: 29 | 30 | ![properties](https://github.com/user-attachments/assets/41f1f460-cbaf-46b5-a994-d1b5d2b38697) 31 | 32 | * **Path**, **Id**, and **Url** are all used internally and should not be modified. To differentiate between multiple cameras, change the name of the *rendy.go* game object instead of its internal components. 33 | * **Active** determines if the camera is rendered to the screen. 34 | * **Orthographic** toggles between an orthographic and perspective projection. Orthographic projections are used for 2D graphics whose dimensions and location on the viewport are not affected by their z position. Perspective projections are used for 3D graphics whose dimensions and location on the viewport are affected by their z positions, similar to how humans see the world through the *perspective* of their eyes. 35 | * (See the next paragraph for details regarding **Resize Mode Center**, **Resize Mode Expand**, and **Resize Mode Stretch**.) 36 | * **Experimental Controls** toggles very basic built-in controller mechanics. If the camera uses an orthographic projection, then the WASD keys can be used to move on the x and y axes. If the camera uses a perspective projection, then the WASD keys can be used to move on the x and z axes, and the mouse can be used to look around. 37 | * **Experimental Speed** sets the speed of the camera if experimental controls are enabled, in pixels per second. 38 | * **Render Order** determines in which order the camera is rendered relative to other cameras. For example, if `camera_main.render_order = 1` and `camera_minimap.render_order = 2`, then the `camera_minimap` will be rendered *after* or *on top of* `camera_main`. 39 | * **Viewport X** and **Viewport Y** set the bottom-left pixel position of the viewport relative to the initial window size specified in the *game.project* file. 40 | * **Viewport Width** and **Viewport Height** set the pixel width and pixel height of the viewport relative to the initial window size specified in the *game.project* file. 41 | * **Resolution Width** and **Resolution Height** set the pixel resolution of the viewport. These should probably be equal or proportional to the **Viewport Width** and **Viewport Height** to achieve a normal aspect ratio. 42 | * **Near** and **Far** set the z position of the frustum's near and far planes. If a game object's z position falls outside these boundaries, then it is not rendered. Orthographic projections usually set these to \[-1, 1], while perspective projections usually set these to \[0.1, 1000]. 43 | * **Zoom** sets the zoom factor for an orthographic projection. For example, if the `zoom = 0.5`, then the camera will zoom in and game objects will appear 2x as large. If `zoom = 2`, then the camera will zoom out and game objects will appear 0.5x as large. (This may seem counterintuitive, however it saves us from coding an extra division operation and I truthfully cannot think of a better term for "opposite of zoom". Please submit a pull request if you come up with something!) 44 | * **Field Of View** effectively sets the zoom factor for a perspective projection, but more precisely, it sets the degrees in width that the camera can see of the game world. For example, if `field_of_view = 45`, then then camera can see a 45-degree window of game objects. A human's field of view is roughly 135 degrees, however we tend to use lower values in video games. 45 | 46 | Import Rendy into script files that need to call camera utility functions: 47 | 48 | ``` 49 | local rendy = require "rendy.rendy" 50 | ``` 51 | 52 | --- 53 | 54 | ### Resize Modes (3 of 7) 55 | 56 | One of the most common problems every game developer must confront is what to do when the user resizes the window. Developing for just one specific screen size is highly unlikely in today's world of running the same software on multiple different devices, resolutions, screen orientations, etc. Rendy offers three *resize modes* to solve this problem. 57 | 58 | The following images show a window whose width has increased by a couple hundred pixels. Its original size was 960 x 540. The camera's viewport size is 100% of the window size, or 960 x 540. The camera's position is defined by the centerpoint of its viewport, which is (480, 270) + an offset of (0, 27) = a final position of (480, 297). The bottom-left of the viewport has a screen position of (0, 0). The Cursor World Position label is not relevant to these examples. 59 | 60 | **Resize Mode Center** centers the viewport on screen, maintains its aspect ratio, and shows a consistent area of the game world regardless of the viewport size. 61 | 62 | ![resize_mode_center](https://github.com/user-attachments/assets/7e7f7223-b35e-42f8-959d-48140556b6ce) 63 | 64 | **Resize Mode Expand** maintains the original size of graphics regardless of the viewport size, and shows more or less of the game world. 65 | 66 | ![resize_mode_expand](https://github.com/user-attachments/assets/a54a06d2-1679-45c0-ab6c-516c7f4719ae) 67 | 68 | **Resize Mode Stretch** employs no intelligent measures for resizing graphics or the viewport. This leads to graphical stretching when the current viewport size does not match the target resolution. 69 | 70 | ![resize_mode_stretch](https://github.com/user-attachments/assets/3b34b4d4-37d2-4b94-9482-3daf0428717e) 71 | 72 | --- 73 | 74 | ### Setting and Getting Properties (4 of 7) 75 | 76 | Use `rendy.set()` instead of `go.set()` to modify camera properties. This function includes a few checks and enhancements that avoid state issues. 77 | 78 | ``` 79 | rendy.set(hash("/my_rendy_object"), "viewport_width", 480) 80 | ``` 81 | 82 | The standard `go.get()` function can be safely used to retrieve property values. The analogous `rendy.get()` function is provided for consistency. 83 | 84 | --- 85 | 86 | ### Shake Effect (5 of 7) 87 | 88 | Shaking the camera is a widely loved feature by developers and players alike. It's just so fun! The camera's x and y positions are always animated, however its z position is only animated if using a perspective projection. Here is a rather slow and exaggerated example of an orthographic camera shake: 89 | 90 | ``` 91 | rendy.shake(hash("/my_rendy_object"), radius = 200, intensity = 10, duration = 2 [, scaler = 0.75]) 92 | ``` 93 | 94 | ![shake](https://github.com/user-attachments/assets/dc23a264-698e-47cd-9073-4706b0cc3aac) 95 | 96 | The camera moves `radius` units in random directions, ping-pongs between its original position and radius-defined position `intensity` times, over a period of `duration` seconds, where each ping-pong distance is multiplied by `scaler`. This optional scaler value is what allows the shake to "calm down" and come to a smooth finish. 97 | 98 | --- 99 | 100 | ### Coordinate Conversions (6 of 7) 101 | 102 | It is often useful to convert between screen coordinates and world coordinates. One practical use-case is clicking on the screen, casting a ray into the game world, then retrieving all of the game object ids which intersect that ray. To accomplish tasks like this one, Rendy provides `screen_to_world()` and `world_to_screen()` functions. 103 | 104 | Defold passes an `action` table to its `on_input()` functions. The `action.x` variable does not reflect changes to the window's size relative to its initial size specified in the game.project file. The `action.screen_x` *does* reflect size changes. For example, if a 960 x 540 window is resized to 1920 x 1080, then moving the cursor to the middle of the screen will show `action.xy = (480, 270)` and `action.screen_xy = (960, 540)`. Rendy expects `action.screen_xy` when passing screen positions to any of its utility functions. 105 | 106 | (Todo: Examples of coordinate-conversion functions.) 107 | 108 | --- 109 | 110 | ### Render Script (7 of 7) 111 | 112 | If your project requires you to modify the pre-packaged *rendy.render_script* file, then remember to change the Render component in the *game.project* file's Bootstrap section. Rendy's render script is thoroughly commented, which will hopefully assist you in adding your own render predicates, swapping OpenGL states, and implementing more advanced graphics features. 113 | 114 | The most likely use-case is adding custom render predicates. The following table is located near the top of the render script: 115 | 116 | ``` 117 | -- Contains all render predicates. 118 | -- Each predicate consists of a table formatted like so: 119 | -- = { tags = { hash(""), ... }, object = nil } 120 | -- The `object` entry will store the actual predicate, which is created by calling `render.predicate()`. 121 | local predicates = 122 | { 123 | -- Declare predicates that come shipped with default Defold components. 124 | model = 125 | { 126 | tags = { hash("model") }, 127 | object = nil 128 | }, 129 | tile = 130 | { 131 | tags = { hash("tile") }, 132 | object = nil 133 | }, 134 | particle = 135 | { 136 | tags = { hash("particle") }, 137 | object = nil 138 | }, 139 | gui = 140 | { 141 | tags = { hash("gui") }, 142 | object = nil 143 | }, 144 | debug_text = 145 | { 146 | tags = { hash("debug_text") }, 147 | object = nil 148 | }, 149 | -- Declare custom predicates. 150 | } 151 | ``` 152 | 153 | Each predicate in this table is created in the `init()` function. The ordering of each predicate does not matter, however to maintain readability, custom predicates should be added to the bottom of the table. As mentioned by the comments, the `object` entry of each predicate table will reference the actual predicate, which can be drawn in the `update()` function: 154 | 155 | ``` 156 | render.draw(predicates.tile.object, { frustum = camera.frustum }) 157 | ``` 158 | 159 | ## API 160 | 161 | ### rendy.create_camera(camera_id) 162 | 163 | Creates a camera. This function is called automatically by the *rendy.go* game object. 164 | 165 | ### rendy.destroy_camera(camera_id) 166 | 167 | Destroys a camera. This function is called automatically by the *rendy.go* game object. 168 | 169 | --- 170 | 171 | ### function rendy.set(camera_id, property, value) 172 | 173 | Sets a camera property. This function replaces the standard `go.set()`. 174 | 175 | ### function rendy.get(camera_id, property) 176 | 177 | Gets a camera property. This function is equivalent to the standard `go.get()`. 178 | 179 | --- 180 | 181 | ### function rendy.get_display_size() 182 | 183 | Gets the initial window size specified in the *game.project* file. 184 | 185 | ### function rendy.get_window_size() 186 | 187 | Gets the current window size. 188 | 189 | ### function rendy.get_stack(screen_x, screen_y) 190 | 191 | Gets camera ids whose viewports intersect a screen position. 192 | 193 | ### function rendy.shake(camera_id, radius, intensity, duration \[, scaler]) 194 | 195 | Starts a camera shake animation. If the camera is already shaking, then the animation is cancelled and restarted. The radius is increased or decreased over time if the scaler argument is ~= 1. 196 | 197 | ### function rendy.cancel_shake(camera_id) 198 | 199 | Cancels an ongoing camera shake animation. 200 | 201 | ### function rendy.screen_to_world(camera_id, screen_position) 202 | 203 | Converts a screen position to a world position. 204 | 205 | ### function rendy.world_to_screen(camera_id, world_position) 206 | 207 | Converts a world position to a screen position. 208 | -------------------------------------------------------------------------------- /examples/orthographic/orthographic.collection: -------------------------------------------------------------------------------- 1 | name: "collection_orthographic" 2 | instances { 3 | id: "rendy" 4 | prototype: "/rendy/rendy.go" 5 | component_properties { 6 | id: "script" 7 | properties { 8 | id: "experimental_controls" 9 | value: "true" 10 | type: PROPERTY_TYPE_BOOLEAN 11 | } 12 | properties { 13 | id: "experimental_speed" 14 | value: "540.0" 15 | type: PROPERTY_TYPE_NUMBER 16 | } 17 | properties { 18 | id: "viewport_height" 19 | value: "540.0" 20 | type: PROPERTY_TYPE_NUMBER 21 | } 22 | properties { 23 | id: "resolution_height" 24 | value: "540.0" 25 | type: PROPERTY_TYPE_NUMBER 26 | } 27 | } 28 | } 29 | scale_along_z: 0 30 | embedded_instances { 31 | id: "script" 32 | data: "components {\n" 33 | " id: \"script\"\n" 34 | " component: \"/examples/orthographic/orthographic.script\"\n" 35 | "}\n" 36 | "" 37 | } 38 | embedded_instances { 39 | id: "logo" 40 | data: "embedded_components {\n" 41 | " id: \"sprite\"\n" 42 | " type: \"sprite\"\n" 43 | " data: \"default_animation: \\\"logo_256\\\"\\n" 44 | "material: \\\"/builtins/materials/sprite.material\\\"\\n" 45 | "textures {\\n" 46 | " sampler: \\\"texture_sampler\\\"\\n" 47 | " texture: \\\"/examples/perspective/perspective.atlas\\\"\\n" 48 | "}\\n" 49 | "\"\n" 50 | "}\n" 51 | "" 52 | } 53 | embedded_instances { 54 | id: "label" 55 | data: "embedded_components {\n" 56 | " id: \"label\"\n" 57 | " type: \"label\"\n" 58 | " data: \"size {\\n" 59 | " x: 480.0\\n" 60 | " y: 64.0\\n" 61 | "}\\n" 62 | "color {\\n" 63 | " x: 0.0\\n" 64 | " y: 0.0\\n" 65 | " z: 0.0\\n" 66 | "}\\n" 67 | "line_break: true\\n" 68 | "font: \\\"/builtins/fonts/debug/always_on_top.font\\\"\\n" 69 | "material: \\\"/builtins/fonts/label.material\\\"\\n" 70 | "\"\n" 71 | "}\n" 72 | "" 73 | position { 74 | y: 202.0 75 | } 76 | scale3 { 77 | x: 2.0 78 | y: 2.0 79 | } 80 | } 81 | embedded_instances { 82 | id: "background" 83 | data: "embedded_components {\n" 84 | " id: \"sprite\"\n" 85 | " type: \"sprite\"\n" 86 | " data: \"default_animation: \\\"anim\\\"\\n" 87 | "material: \\\"/builtins/materials/sprite.material\\\"\\n" 88 | "textures {\\n" 89 | " sampler: \\\"texture_sampler\\\"\\n" 90 | " texture: \\\"/builtins/graphics/particle_blob.tilesource\\\"\\n" 91 | "}\\n" 92 | "\"\n" 93 | " scale {\n" 94 | " x: 100.0\n" 95 | " y: 100.0\n" 96 | " }\n" 97 | "}\n" 98 | "" 99 | position { 100 | z: -0.1 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/orthographic/orthographic.script: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- License 3 | -------------------------------------------------------------------------------- 4 | 5 | -- Copyright (c) 2024 White Box Dev 6 | 7 | -- This software is provided 'as-is', without any express or implied warranty. 8 | -- In no event will the authors be held liable for any damages arising from the use of this software. 9 | 10 | -- Permission is granted to anyone to use this software for any purpose, 11 | -- including commercial applications, and to alter it and redistribute it freely, 12 | -- subject to the following restrictions: 13 | 14 | -- 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. 15 | -- If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 16 | 17 | -- 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 18 | 19 | -- 3. This notice may not be removed or altered from any source distribution. 20 | 21 | -------------------------------------------------------------------------------- 22 | -- Information 23 | -------------------------------------------------------------------------------- 24 | 25 | -- GitHub: https://github.com/whiteboxdev/library-defold-rendy 26 | 27 | -------------------------------------------------------------------------------- 28 | -- Dependencies 29 | -------------------------------------------------------------------------------- 30 | 31 | local rendy = require "rendy.rendy" 32 | 33 | -------------------------------------------------------------------------------- 34 | -- Constants 35 | -------------------------------------------------------------------------------- 36 | 37 | local message_acquire_input_focus = hash("acquire_input_focus") 38 | 39 | local id_label = hash("/label") 40 | local id_logo = hash("/logo") 41 | local id_rendy = hash("/rendy") 42 | 43 | local key_1 = hash("key_1") 44 | local key_2 = hash("key_2") 45 | local key_3 = hash("key_3") 46 | local key_4 = hash("key_4") 47 | local key_esc = hash("key_esc") 48 | 49 | -------------------------------------------------------------------------------- 50 | -- Variables 51 | -------------------------------------------------------------------------------- 52 | 53 | local action_screen_x = nil 54 | local action_screen_y = nil 55 | 56 | -------------------------------------------------------------------------------- 57 | -- Local Functions 58 | -------------------------------------------------------------------------------- 59 | 60 | local function update_label() 61 | local text = "" 62 | -- Stringify the camera's resize mode. 63 | if rendy.get(id_rendy, "resize_mode_center") then 64 | text = text .. "Resize Mode: Center" 65 | elseif rendy.get(id_rendy, "resize_mode_expand") then 66 | text = text .. "Resize Mode: Expand" 67 | elseif rendy.get(id_rendy, "resize_mode_stretch") then 68 | text = text .. "Resize Mode: Stretch" 69 | end 70 | -- Stringify the logo's screen position. 71 | local screen_position = rendy.world_to_screen(id_rendy, go.get_position(id_logo)) 72 | if screen_position then 73 | text = text .. "\n" .. "Logo Screen Position: (" .. string.format("%.0f", screen_position.x) .. ", " .. string.format("%.0f", screen_position.y) .. ")" 74 | else 75 | text = text .. "\n" .. "Logo Screen Position: (nil, nil)" 76 | end 77 | -- Stringify the cursor's world position. 78 | local world_position = rendy.screen_to_world(id_rendy, vmath.vector3(action_screen_x, action_screen_y, 0)) 79 | if world_position then 80 | text = text .. "\n" .. "Cursor World Position: (" .. string.format("%.0f", world_position.x) .. ", " .. string.format("%.0f", world_position.y) .. ")" 81 | else 82 | text = text .. "\n" .. "Cursor World Position: (nil, nil)" 83 | end 84 | -- Update the label. 85 | label.set_text(id_label, text) 86 | end 87 | 88 | -------------------------------------------------------------------------------- 89 | -- Engine Functions 90 | -------------------------------------------------------------------------------- 91 | 92 | function init() 93 | msg.post(msg.url(), message_acquire_input_focus) 94 | go.animate(id_logo, "euler.z", go.PLAYBACK_LOOP_BACKWARD, 360, go.EASING_LINEAR, 3) 95 | end 96 | 97 | function update() 98 | update_label() 99 | end 100 | 101 | function on_input(_, action_id, action) 102 | -- Get the cursor's position. 103 | if not action_id then 104 | action_screen_x = action.screen_x 105 | action_screen_y = action.screen_y 106 | end 107 | -- Update the camera's resize mode. 108 | if action.pressed then 109 | if action_id == key_1 then 110 | rendy.set(id_rendy, "resize_mode_center", true) 111 | elseif action_id == key_2 then 112 | rendy.set(id_rendy, "resize_mode_expand", true) 113 | elseif action_id == key_3 then 114 | rendy.set(id_rendy, "resize_mode_stretch", true) 115 | elseif action_id == key_4 then 116 | rendy.shake(id_rendy, 150, 10, 0.5, 0.75) 117 | elseif action_id == key_esc then 118 | sys.exit(0) 119 | end 120 | end 121 | end -------------------------------------------------------------------------------- /examples/perspective/perspective.atlas: -------------------------------------------------------------------------------- 1 | images { 2 | image: "/builtins/assets/images/logo/logo_256.png" 3 | sprite_trim_mode: SPRITE_TRIM_MODE_OFF 4 | } 5 | margin: 0 6 | extrude_borders: 2 7 | inner_padding: 0 8 | max_page_width: 0 9 | max_page_height: 0 10 | rename_patterns: "" 11 | -------------------------------------------------------------------------------- /examples/perspective/perspective.collection: -------------------------------------------------------------------------------- 1 | name: "collection_perspective" 2 | instances { 3 | id: "rendy" 4 | prototype: "/rendy/rendy.go" 5 | position { 6 | z: 5.0 7 | } 8 | component_properties { 9 | id: "script" 10 | properties { 11 | id: "orthographic" 12 | value: "false" 13 | type: PROPERTY_TYPE_BOOLEAN 14 | } 15 | properties { 16 | id: "experimental_controls" 17 | value: "true" 18 | type: PROPERTY_TYPE_BOOLEAN 19 | } 20 | properties { 21 | id: "experimental_speed" 22 | value: "5.0" 23 | type: PROPERTY_TYPE_NUMBER 24 | } 25 | properties { 26 | id: "viewport_height" 27 | value: "540.0" 28 | type: PROPERTY_TYPE_NUMBER 29 | } 30 | properties { 31 | id: "resolution_height" 32 | value: "540.0" 33 | type: PROPERTY_TYPE_NUMBER 34 | } 35 | properties { 36 | id: "z_min" 37 | value: "0.1" 38 | type: PROPERTY_TYPE_NUMBER 39 | } 40 | properties { 41 | id: "z_max" 42 | value: "1000.0" 43 | type: PROPERTY_TYPE_NUMBER 44 | } 45 | } 46 | } 47 | scale_along_z: 0 48 | embedded_instances { 49 | id: "logo" 50 | data: "embedded_components {\n" 51 | " id: \"model\"\n" 52 | " type: \"model\"\n" 53 | " data: \"mesh: \\\"/builtins/assets/meshes/cube.dae\\\"\\n" 54 | "name: \\\"{{NAME}}\\\"\\n" 55 | "materials {\\n" 56 | " name: \\\"default\\\"\\n" 57 | " material: \\\"/builtins/materials/model.material\\\"\\n" 58 | " textures {\\n" 59 | " sampler: \\\"tex0\\\"\\n" 60 | " texture: \\\"/builtins/assets/images/logo/logo_256.png\\\"\\n" 61 | " }\\n" 62 | "}\\n" 63 | "\"\n" 64 | "}\n" 65 | "" 66 | } 67 | embedded_instances { 68 | id: "script" 69 | data: "components {\n" 70 | " id: \"script\"\n" 71 | " component: \"/examples/perspective/perspective.script\"\n" 72 | "}\n" 73 | "" 74 | } 75 | -------------------------------------------------------------------------------- /examples/perspective/perspective.script: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- License 3 | -------------------------------------------------------------------------------- 4 | 5 | -- Copyright (c) 2024 White Box Dev 6 | 7 | -- This software is provided 'as-is', without any express or implied warranty. 8 | -- In no event will the authors be held liable for any damages arising from the use of this software. 9 | 10 | -- Permission is granted to anyone to use this software for any purpose, 11 | -- including commercial applications, and to alter it and redistribute it freely, 12 | -- subject to the following restrictions: 13 | 14 | -- 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. 15 | -- If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 16 | 17 | -- 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 18 | 19 | -- 3. This notice may not be removed or altered from any source distribution. 20 | 21 | -------------------------------------------------------------------------------- 22 | -- Information 23 | -------------------------------------------------------------------------------- 24 | 25 | -- GitHub: https://github.com/whiteboxdev/library-defold-rendy 26 | 27 | -------------------------------------------------------------------------------- 28 | -- Dependencies 29 | -------------------------------------------------------------------------------- 30 | 31 | local rendy = require "rendy.rendy" 32 | 33 | -------------------------------------------------------------------------------- 34 | -- Constants 35 | -------------------------------------------------------------------------------- 36 | 37 | local message_acquire_input_focus = hash("acquire_input_focus") 38 | 39 | local key_1 = hash("key_1") 40 | local key_2 = hash("key_2") 41 | local key_3 = hash("key_3") 42 | local key_4 = hash("key_4") 43 | local key_backspace = hash("key_backspace") 44 | local key_esc = hash("key_esc") 45 | 46 | local id_logo = hash("/logo") 47 | local id_rendy = hash("/rendy") 48 | 49 | -------------------------------------------------------------------------------- 50 | -- Engine Functions 51 | -------------------------------------------------------------------------------- 52 | 53 | function init() 54 | window.set_mouse_lock(true) 55 | go.animate(id_logo, "euler.x", go.PLAYBACK_LOOP_FORWARD, 360, go.EASING_LINEAR, 10) 56 | go.animate(id_logo, "euler.y", go.PLAYBACK_LOOP_FORWARD, 360, go.EASING_LINEAR, 10) 57 | go.animate(id_logo, "euler.z", go.PLAYBACK_LOOP_FORWARD, 360, go.EASING_LINEAR, 10) 58 | msg.post(msg.url(), message_acquire_input_focus) 59 | end 60 | 61 | function update() 62 | local camera_position = go.get_position(id_rendy) 63 | go.set(msg.url(nil, id_logo, "model"), "light", vmath.vector4(camera_position.x, camera_position.y, camera_position.z, 0)) 64 | end 65 | 66 | function on_input(_, action_id, action) 67 | if action.pressed then 68 | if action_id == key_1 then 69 | rendy.set(id_rendy, "resize_mode_center", true) 70 | elseif action_id == key_2 then 71 | rendy.set(id_rendy, "resize_mode_expand", true) 72 | elseif action_id == key_3 then 73 | rendy.set(id_rendy, "resize_mode_stretch", true) 74 | elseif action_id == key_4 then 75 | rendy.shake(id_rendy, 0.5, 5, 0.5, 0.75) 76 | elseif action_id == key_backspace then 77 | window.set_mouse_lock(not window.get_mouse_lock()) 78 | elseif action_id == key_esc then 79 | sys.exit(0) 80 | end 81 | end 82 | end -------------------------------------------------------------------------------- /game.project: -------------------------------------------------------------------------------- 1 | [bootstrap] 2 | main_collection = /examples/orthographic/orthographic.collectionc 3 | render = /rendy/rendy.renderc 4 | 5 | [script] 6 | shared_state = 1 7 | 8 | [display] 9 | width = 960 10 | height = 540 11 | 12 | [android] 13 | input_method = HiddenInputField 14 | 15 | [project] 16 | title = Defold Rendy 17 | version = 1.0.0 18 | publisher = White Box Dev 19 | developer = White Box Dev 20 | 21 | [input] 22 | game_binding = /builtins/input/all.input_bindingc 23 | 24 | [library] 25 | include_dirs = rendy 26 | 27 | -------------------------------------------------------------------------------- /rendy/rendy.camera: -------------------------------------------------------------------------------- 1 | aspect_ratio: 1.0 2 | fov: 0.7854 3 | near_z: -1.0 4 | far_z: 1.0 5 | auto_aspect_ratio: 0 6 | orthographic_projection: 1 7 | orthographic_zoom: 1.0 8 | -------------------------------------------------------------------------------- /rendy/rendy.go: -------------------------------------------------------------------------------- 1 | components { 2 | id: "camera" 3 | component: "/rendy/rendy.camera" 4 | position { 5 | x: 0.0 6 | y: 0.0 7 | z: 0.0 8 | } 9 | rotation { 10 | x: 0.0 11 | y: 0.0 12 | z: 0.0 13 | w: 1.0 14 | } 15 | } 16 | components { 17 | id: "script" 18 | component: "/rendy/rendy.script" 19 | position { 20 | x: 0.0 21 | y: 0.0 22 | z: 0.0 23 | } 24 | rotation { 25 | x: 0.0 26 | y: 0.0 27 | z: 0.0 28 | w: 1.0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rendy/rendy.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- License 3 | -------------------------------------------------------------------------------- 4 | 5 | -- Copyright (c) 2024 White Box Dev 6 | 7 | -- This software is provided 'as-is', without any express or implied warranty. 8 | -- In no event will the authors be held liable for any damages arising from the use of this software. 9 | 10 | -- Permission is granted to anyone to use this software for any purpose, 11 | -- including commercial applications, and to alter it and redistribute it freely, 12 | -- subject to the following restrictions: 13 | 14 | -- 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. 15 | -- If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 16 | 17 | -- 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 18 | 19 | -- 3. This notice may not be removed or altered from any source distribution. 20 | 21 | -------------------------------------------------------------------------------- 22 | -- Information 23 | -------------------------------------------------------------------------------- 24 | 25 | -- GitHub: https://github.com/whiteboxdev/library-defold-rendy 26 | 27 | -------------------------------------------------------------------------------- 28 | -- Constants 29 | -------------------------------------------------------------------------------- 30 | 31 | local message_acquire_camera_focus = hash("acquire_camera_focus") 32 | local message_release_camera_focus = hash("release_camera_focus") 33 | 34 | local message_acquire_input_focus = hash("acquire_input_focus") 35 | local message_release_input_focus = hash("release_input_focus") 36 | 37 | -------------------------------------------------------------------------------- 38 | -- Variables 39 | -------------------------------------------------------------------------------- 40 | 41 | local rendy = {} 42 | 43 | -- The following `rendy._` variables should not be directly accessed or manipulated by the user. 44 | -- They are only exposed because the rendy.render_script file needs write-access to them. 45 | 46 | -- Contains all cameras except the GUI camera, which is stored in the rendy.render_script file. 47 | -- { [camera_id] = , ... } 48 | rendy.cameras = {} 49 | 50 | -- Initial width and height of the window, specified in the game.project file. 51 | rendy.display_width = nil 52 | rendy.display_height = nil 53 | 54 | -- Current width and height of the window. 55 | rendy.window_width = nil 56 | rendy.window_height = nil 57 | 58 | -- Checks if a screen position in within a camera's viewport. 59 | local function is_within_viewport(camera, screen_x, screen_y) 60 | return 61 | camera.viewport_pixel_x <= screen_x and 62 | screen_x <= camera.viewport_pixel_x + camera.viewport_pixel_width and 63 | camera.viewport_pixel_y <= screen_y and 64 | screen_y <= camera.viewport_pixel_y + camera.viewport_pixel_height 65 | end 66 | 67 | -- Checks if an NDC position is within the NDC cube. 68 | local function is_within_ndc_cube(ndc_position) 69 | return 70 | -1 <= ndc_position.x and ndc_position.x <= 1 and 71 | -1 <= ndc_position.y and ndc_position.y <= 1 and 72 | -1 <= ndc_position.z and ndc_position.z <= 1 73 | end 74 | 75 | -------------------------------------------------------------------------------- 76 | -- Module Functions 77 | -------------------------------------------------------------------------------- 78 | 79 | -- Creates a camera. 80 | -- This function is called automatically by the rendy.go game object. 81 | function rendy.create_camera(camera_id) 82 | local camera_url = msg.url(nil, camera_id, "camera") 83 | local script_url = msg.url(nil, camera_id, "script") 84 | rendy.cameras[camera_id] = 85 | { 86 | -- Variables that are not configured by the developer. 87 | camera_id = camera_id, 88 | camera_url = msg.url(nil, camera_id, "camera"), 89 | script_url = msg.url(nil, camera_id, "script"), 90 | viewport_pixel_x = 0, 91 | viewport_pixel_y = 0, 92 | viewport_pixel_width = 0, 93 | viewport_pixel_height = 0, 94 | view_transform = vmath.matrix4(), 95 | projection_transform = vmath.matrix4(), 96 | frustum = vmath.matrix4(), 97 | shake_timer = nil, 98 | shake_position = nil, 99 | -- Variables that are configured by the developer in the editor. 100 | active = go.get(script_url, "active"), 101 | orthographic = go.get(script_url, "orthographic"), 102 | resize_mode_center = go.get(script_url, "resize_mode_center"), 103 | resize_mode_expand = go.get(script_url, "resize_mode_expand"), 104 | resize_mode_stretch = go.get(script_url, "resize_mode_stretch"), 105 | experimental_controls = go.get(script_url, "experimental_controls"), 106 | experimental_speed = go.get(script_url, "experimental_speed"), 107 | render_order = go.get(script_url, "render_order"), 108 | viewport_x = go.get(script_url, "viewport_x"), 109 | viewport_y = go.get(script_url, "viewport_y"), 110 | viewport_width = go.get(script_url, "viewport_width"), 111 | viewport_height = go.get(script_url, "viewport_height"), 112 | resolution_width = go.get(script_url, "resolution_width"), 113 | resolution_height = go.get(script_url, "resolution_height"), 114 | z_min = go.get(script_url, "z_min"), 115 | z_max = go.get(script_url, "z_max"), 116 | zoom = go.get(script_url, "zoom"), 117 | field_of_view = go.get(script_url, "field_of_view") 118 | } 119 | msg.post(camera_url, message_acquire_camera_focus) 120 | if rendy.cameras[camera_id].experimental_controls then 121 | msg.post(script_url, message_acquire_input_focus) 122 | end 123 | end 124 | 125 | -- Destroys a camera. 126 | -- This function is called automatically by the rendy.go game object. 127 | function rendy.destroy_camera(camera_id) 128 | msg.post(rendy.cameras[camera_id].camera_url, message_release_camera_focus) 129 | if rendy.cameras[camera_id].experimental_controls then 130 | msg.post(rendy.cameras[camera_id].script_url, message_release_input_focus) 131 | end 132 | rendy.cameras[camera_id] = nil 133 | end 134 | 135 | -- Sets a camera property. 136 | -- This function replaces the standard `go.set()`. 137 | function rendy.set(camera_id, property, value) 138 | if rendy.cameras[camera_id][property] == nil then 139 | print("Defold Rendy: rendy.set() -> Unknown property: " .. property) 140 | return 141 | end 142 | if property == "resize_mode_center" or property == "resize_mode_expand" or property == "resize_mode_stretch" then 143 | rendy.cameras[camera_id].resize_mode_center = false 144 | rendy.cameras[camera_id].resize_mode_expand = false 145 | rendy.cameras[camera_id].resize_mode_stretch = false 146 | end 147 | if property == "experimental_controls" then 148 | msg.post(rendy.cameras[camera_id].script_url, value and message_acquire_input_focus or message_release_input_focus) 149 | end 150 | rendy.cameras[camera_id][property] = value 151 | end 152 | 153 | -- Gets a camera property. 154 | -- This function is equivalent to the standard `go.get()`. 155 | function rendy.get(camera_id, property) 156 | return rendy.cameras[camera_id][property] 157 | end 158 | 159 | -- Gets the initial window size specified in the game.project file. 160 | function rendy.get_display_size() 161 | return vmath.vector3(rendy.display_width, rendy.display_height, 0) 162 | end 163 | 164 | -- Gets the current window size. 165 | function rendy.get_window_size() 166 | return vmath.vector3(rendy.window_width, rendy.window_height, 0) 167 | end 168 | 169 | -- Gets camera ids whose viewports intersect with a screen position. 170 | function rendy.get_stack(screen_x, screen_y) 171 | local camera_ids = {} 172 | for camera_id, camera in pairs(rendy.cameras) do 173 | if is_within_viewport(camera, screen_x, screen_y) then 174 | camera_ids[#camera_ids + 1] = camera_id 175 | end 176 | end 177 | return camera_ids 178 | end 179 | 180 | -- Starts a shake animation. 181 | -- If the camera is already shaking, then the animation is cancelled and restarted. 182 | -- The radius is increased or decreased over time if the scaler argument is ~= 1. 183 | function rendy.shake(camera_id, radius, intensity, duration, scaler) 184 | if rendy.cameras[camera_id].shake_timer then 185 | rendy.cancel_shake(camera_id) 186 | end 187 | rendy.cameras[camera_id].shake_position = go.get_position(camera_id) 188 | local shake_duration = duration / intensity 189 | local animate = function() 190 | local milliseconds = socket.gettime() * 1000 191 | local position_offset_x = math.sin(milliseconds) 192 | local position_offset_y = math.cos(milliseconds) 193 | local position_offset_z = not rendy.cameras[camera_id].orthographic and position_offset_x * position_offset_y or 0 194 | local to = rendy.cameras[camera_id].shake_position + vmath.vector3(position_offset_x, position_offset_y, position_offset_z) * radius 195 | go.set_position(rendy.cameras[camera_id].shake_position, camera_id) 196 | go.animate(camera_id, "position", go.PLAYBACK_ONCE_PINGPONG, to, go.EASING_LINEAR, shake_duration) 197 | end 198 | animate() 199 | local animation_loop = function() 200 | intensity = intensity - 1 201 | radius = radius * (scaler or 1) 202 | if intensity > 0 then 203 | animate() 204 | else 205 | rendy.cancel_shake(camera_id) 206 | end 207 | end 208 | rendy.cameras[camera_id].shake_timer = timer.delay(shake_duration, true, animation_loop) 209 | end 210 | 211 | -- Cancels an ongoing shake animation. 212 | function rendy.cancel_shake(camera_id) 213 | if not rendy.cameras[camera_id].shake_timer then 214 | return 215 | end 216 | timer.cancel(rendy.cameras[camera_id].shake_timer) 217 | go.cancel_animations(camera_id, "position") 218 | go.set_position(rendy.cameras[camera_id].shake_position, camera_id) 219 | rendy.cameras[camera_id].shake_timer = nil 220 | rendy.cameras[camera_id].shake_position = nil 221 | end 222 | 223 | -- Converts a screen position to a world position. 224 | -- The screen position's z component maps to the camera frustum's z component. 225 | function rendy.screen_to_world(camera_id, screen_position) 226 | if not is_within_viewport(rendy.cameras[camera_id], screen_position.x, screen_position.y) then 227 | return 228 | end 229 | -- Multiplying by the inverse frustum reverts the projection and view matrix transformations. 230 | local inverse_frustum = vmath.inv(rendy.cameras[camera_id].frustum) 231 | -- Clip coordinates tell us where the screen position is located in the NDC cube, between [-1, 1] on all axes. 232 | -- For example, if the screen position is on the left side of the viewport, then its `clip_x` would be -1. 233 | local clip_x = (screen_position.x - rendy.cameras[camera_id].viewport_pixel_x) / rendy.cameras[camera_id].viewport_pixel_width * 2 - 1 234 | local clip_y = (screen_position.y - rendy.cameras[camera_id].viewport_pixel_y) / rendy.cameras[camera_id].viewport_pixel_height * 2 - 1 235 | -- Convert the screen position to (1) a world position on the near plane, and (2) a world position on the far plane. 236 | local near_world_position = vmath.vector4(inverse_frustum * vmath.vector4(clip_x, clip_y, -1, 1)) 237 | local far_world_position = vmath.vector4(inverse_frustum * vmath.vector4(clip_x, clip_y, 1, 1)) 238 | -- Remove the homogeneous coordinate. 239 | near_world_position = near_world_position / near_world_position.w 240 | far_world_position = far_world_position / far_world_position.w 241 | -- Calculate world position's z component between the near and far planes. 242 | local frustum_z = (screen_position.z - rendy.cameras[camera_id].z_min) / (rendy.cameras[camera_id].z_max - rendy.cameras[camera_id].z_min) 243 | local world_position = vmath.lerp(frustum_z, near_world_position, far_world_position) 244 | -- Strip the meaningless w component from the world position. 245 | return vmath.vector3(world_position.x, world_position.y, world_position.z) 246 | end 247 | 248 | -- Converts a world position to a screen position. 249 | -- The world position's z component maps to the camera frustum's z component. 250 | function rendy.world_to_screen(camera_id, world_position) 251 | local ndc_position = rendy.cameras[camera_id].frustum * vmath.vector4(world_position.x, world_position.y, world_position.z, 1) / rendy.cameras[camera_id].frustum.m3.w 252 | if not is_within_ndc_cube(ndc_position) then 253 | return 254 | end 255 | local screen_x = (ndc_position.x + 1) * rendy.cameras[camera_id].viewport_pixel_width * 0.5 256 | local screen_y = (ndc_position.y + 1) * rendy.cameras[camera_id].viewport_pixel_height * 0.5 257 | return vmath.vector3(screen_x, screen_y, 0) 258 | end 259 | 260 | return rendy -------------------------------------------------------------------------------- /rendy/rendy.render: -------------------------------------------------------------------------------- 1 | script: "/rendy/rendy.render_script" 2 | -------------------------------------------------------------------------------- /rendy/rendy.render_script: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- License 3 | -------------------------------------------------------------------------------- 4 | 5 | -- Copyright (c) 2024 White Box Dev 6 | 7 | -- This software is provided 'as-is', without any express or implied warranty. 8 | -- In no event will the authors be held liable for any damages arising from the use of this software. 9 | 10 | -- Permission is granted to anyone to use this software for any purpose, 11 | -- including commercial applications, and to alter it and redistribute it freely, 12 | -- subject to the following restrictions: 13 | 14 | -- 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. 15 | -- If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 16 | 17 | -- 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 18 | 19 | -- 3. This notice may not be removed or altered from any source distribution. 20 | 21 | -------------------------------------------------------------------------------- 22 | -- Information 23 | -------------------------------------------------------------------------------- 24 | 25 | -- GitHub: https://github.com/whiteboxdev/library-defold-rendy 26 | 27 | -------------------------------------------------------------------------------- 28 | -- Dependencies 29 | -------------------------------------------------------------------------------- 30 | 31 | local rendy = require "rendy.rendy" 32 | 33 | -------------------------------------------------------------------------------- 34 | -- Constants 35 | -------------------------------------------------------------------------------- 36 | 37 | local message_set_view_projection = hash("set_view_projection") 38 | local message_window_resized = hash("window_resized") 39 | 40 | -------------------------------------------------------------------------------- 41 | -- Variables 42 | -------------------------------------------------------------------------------- 43 | 44 | -- Contains all render predicates. 45 | -- Each predicate consists of a table formatted like so: 46 | -- = { tags = { hash(""), ... }, object = nil } 47 | -- The `object` entry will reference the actual predicate, which is created by calling `render.predicate()`. 48 | local predicates = 49 | { 50 | -- Declare predicates that come shipped with default Defold components. 51 | model = 52 | { 53 | tags = { hash("model") }, 54 | object = nil 55 | }, 56 | tile = 57 | { 58 | tags = { hash("tile") }, 59 | object = nil 60 | }, 61 | particle = 62 | { 63 | tags = { hash("particle") }, 64 | object = nil 65 | }, 66 | gui = 67 | { 68 | tags = { hash("gui") }, 69 | object = nil 70 | }, 71 | debug_text = 72 | { 73 | tags = { hash("debug_text") }, 74 | object = nil 75 | }, 76 | -- Declare custom predicates. 77 | } 78 | 79 | -- Default color, depth, and stencil buffer values that are passed to `render.clear()`. 80 | local clear_buffers = nil 81 | 82 | -- The GUI camera is not created through `rendy.create_camera()` because: 83 | -- (1) it is not coupled to a game object, 84 | -- (2) it does not require the same data fields that a normal camera requires, and 85 | -- (3) it does not support many of the features that a normal camera supports. 86 | -- There is only one GUI camera in existence, it is always active, and it has a unique resize mode that defers most of the complexity to Defold's GUI component. 87 | local camera_gui = nil 88 | 89 | -------------------------------------------------------------------------------- 90 | -- Local Functions 91 | -------------------------------------------------------------------------------- 92 | 93 | -- Applies the center resize mode to a camera. 94 | -- This resize mode maintains the aspect ratio of the viewport and projection, 95 | -- while maximizing its size relative to the size of the window. 96 | local function apply_resize_mode_center(camera) 97 | local window_width_ratio = rendy.window_width / rendy.window_height 98 | local window_height_ratio = rendy.window_height / rendy.window_width 99 | local resolution_width_ratio = camera.resolution_width / camera.resolution_height 100 | local resolution_height_ratio = camera.resolution_height / camera.resolution_width 101 | local viewport_fraction_x = camera.viewport_x / rendy.display_width 102 | local viewport_fraction_y = camera.viewport_y / rendy.display_height 103 | local viewport_fraction_width = camera.viewport_width / rendy.display_width 104 | local viewport_fraction_height = camera.viewport_height / rendy.display_height 105 | -- Calculate the width of the bars on the left and right sides of the window. 106 | -- These bars are only necessary if the window width exceeds its target resolution. 107 | local margin_width = window_width_ratio - resolution_width_ratio > 0 and (window_width_ratio - resolution_width_ratio) * rendy.window_height * viewport_fraction_width or 0 108 | -- Calculate the height of the bars on the top and bottom sides of the window. 109 | -- These bars are only necessary if the window height exceeds its target resolution. 110 | local margin_height = window_height_ratio - resolution_height_ratio > 0 and (window_height_ratio - resolution_height_ratio) * rendy.window_width * viewport_fraction_height or 0 111 | camera.viewport_pixel_x = rendy.window_width * viewport_fraction_x + margin_width * 0.5 112 | camera.viewport_pixel_y = rendy.window_height * viewport_fraction_y + margin_height * 0.5 113 | camera.viewport_pixel_width = rendy.window_width * viewport_fraction_width - margin_width 114 | camera.viewport_pixel_height = rendy.window_height * viewport_fraction_height - margin_height 115 | if camera.orthographic then 116 | local left = -camera.resolution_width * 0.5 * camera.zoom 117 | local right = camera.resolution_width * 0.5 * camera.zoom 118 | local bottom = -camera.resolution_height * 0.5 * camera.zoom 119 | local top = camera.resolution_height * 0.5 * camera.zoom 120 | camera.projection_transform = vmath.matrix4_orthographic(left, right, bottom, top, camera.z_min, camera.z_max) 121 | else 122 | local fov = math.rad(camera.field_of_view) 123 | local aspect = camera.resolution_width / camera.resolution_height 124 | camera.projection_transform = vmath.matrix4_perspective(fov, aspect, camera.z_min, camera.z_max) 125 | end 126 | camera.frustum = camera.projection_transform * camera.view_transform 127 | end 128 | 129 | -- Applies the expand resize mode to a camera. 130 | -- This resize mode stretches the viewport relative to the size of the window, 131 | -- but expands the projection such that more or less of the game world is visible. 132 | local function apply_resize_mode_expand(camera) 133 | local viewport_fraction_x = camera.viewport_x / rendy.display_width 134 | local viewport_fraction_y = camera.viewport_y / rendy.display_height 135 | local viewport_fraction_width = camera.viewport_width / rendy.display_width 136 | local viewport_fraction_height = camera.viewport_height / rendy.display_height 137 | camera.viewport_pixel_x = rendy.window_width * viewport_fraction_x 138 | camera.viewport_pixel_y = rendy.window_height * viewport_fraction_y 139 | camera.viewport_pixel_width = rendy.window_width * viewport_fraction_width 140 | camera.viewport_pixel_height = rendy.window_height * viewport_fraction_height 141 | if camera.orthographic then 142 | local left = -rendy.window_width * 0.5 * camera.zoom 143 | local right = rendy.window_width * 0.5 * camera.zoom 144 | local bottom = -rendy.window_height * 0.5 * camera.zoom 145 | local top = rendy.window_height * 0.5 * camera.zoom 146 | camera.projection_transform = vmath.matrix4_orthographic(left, right, bottom, top, camera.z_min, camera.z_max) 147 | else 148 | local fov = math.rad(camera.field_of_view) 149 | local aspect = rendy.window_width / rendy.window_height 150 | camera.projection_transform = vmath.matrix4_perspective(fov, aspect, camera.z_min, camera.z_max) 151 | end 152 | camera.frustum = camera.projection_transform * camera.view_transform 153 | end 154 | 155 | -- Applies the stretch resize mode to a camera. 156 | -- This resize mode stretches the viewport and projection relative to the size of the window. 157 | local function apply_resize_mode_stretch(camera) 158 | local viewport_fraction_x = camera.viewport_x / rendy.display_width 159 | local viewport_fraction_y = camera.viewport_y / rendy.display_height 160 | local viewport_fraction_width = camera.viewport_width / rendy.display_width 161 | local viewport_fraction_height = camera.viewport_height / rendy.display_height 162 | camera.viewport_pixel_x = rendy.window_width * viewport_fraction_x 163 | camera.viewport_pixel_y = rendy.window_height * viewport_fraction_y 164 | camera.viewport_pixel_width = rendy.window_width * viewport_fraction_width 165 | camera.viewport_pixel_height = rendy.window_height * viewport_fraction_height 166 | if camera.orthographic then 167 | local left = -camera.resolution_width * 0.5 * camera.zoom 168 | local right = camera.resolution_width * 0.5 * camera.zoom 169 | local bottom = -camera.resolution_height * 0.5 * camera.zoom 170 | local top = camera.resolution_height * 0.5 * camera.zoom 171 | camera.projection_transform = vmath.matrix4_orthographic(left, right, bottom, top, camera.z_min, camera.z_max) 172 | else 173 | local fov = math.rad(camera.field_of_view) 174 | local aspect = camera.resolution_width / camera.resolution_height 175 | camera.projection_transform = vmath.matrix4_perspective(fov, aspect, camera.z_min, camera.z_max) 176 | end 177 | camera.frustum = camera.projection_transform * camera.view_transform 178 | end 179 | 180 | -- Applies the GUI resize mode to the GUI camera. 181 | -- This resize mode stretches the camera's viewport and projection to the exact size of the window. 182 | -- Defold's GUI component handles resize calculations. 183 | local function apply_resize_mode_gui() 184 | camera_gui.viewport_pixel_x = 0 185 | camera_gui.viewport_pixel_y = 0 186 | camera_gui.viewport_pixel_width = rendy.window_width 187 | camera_gui.viewport_pixel_height = rendy.window_height 188 | camera_gui.projection_transform = vmath.matrix4_orthographic(0, rendy.window_width, 0, rendy.window_height, -1, 1) 189 | camera_gui.frustum = camera_gui.projection_transform * camera_gui.view_transform 190 | end 191 | 192 | -- Responds to the `set_view_projection` message, which is sent by each camera once per frame. 193 | local function set_view_projection_callback(camera_url, view_transform) 194 | for _, camera in pairs(rendy.cameras) do 195 | if camera_url == camera.camera_url then 196 | camera.view_transform = view_transform 197 | if camera.resize_mode_center then 198 | apply_resize_mode_center(camera) 199 | elseif camera.resize_mode_expand then 200 | apply_resize_mode_expand(camera) 201 | elseif camera.resize_mode_stretch then 202 | apply_resize_mode_stretch(camera) 203 | end 204 | end 205 | end 206 | end 207 | 208 | -- Responds to a `window_resized` message. 209 | local function window_resized_callback() 210 | rendy.window_width = render.get_window_width() 211 | rendy.window_height = render.get_window_height() 212 | apply_resize_mode_gui() 213 | end 214 | 215 | -- Activates a camera. 216 | local function activate_camera(camera) 217 | render.set_viewport(camera.viewport_pixel_x, camera.viewport_pixel_y, camera.viewport_pixel_width, camera.viewport_pixel_height) 218 | render.set_view(camera.view_transform) 219 | render.set_projection(camera.projection_transform) 220 | end 221 | 222 | -- Gets a sorted list of camera ids based on their render orders. 223 | local function get_ordered_camera_ids() 224 | local ids = {} 225 | for id, camera in pairs(rendy.cameras) do 226 | ids[camera.render_order] = id 227 | end 228 | return ids 229 | end 230 | 231 | -------------------------------------------------------------------------------- 232 | -- Engine Functions 233 | -------------------------------------------------------------------------------- 234 | 235 | function init(self) 236 | -- Initialize Rendy variables. 237 | rendy.display_width = sys.get_config_int("display.width") 238 | rendy.display_height = sys.get_config_int("display.height") 239 | rendy.window_width = render.get_window_width() 240 | rendy.window_height = render.get_window_height() 241 | -- Create render predicates. 242 | for _, predicate in pairs(predicates) do 243 | predicate.object = render.predicate(predicate.tags) 244 | end 245 | -- Create and update the GUI camera. 246 | camera_gui = 247 | { 248 | viewport_pixel_x = 0, 249 | viewport_pixel_y = 0, 250 | viewport_pixel_width = 0, 251 | viewport_pixel_height = 0, 252 | view_transform = vmath.matrix4(), 253 | projection_transform = vmath.matrix4(), 254 | frustum = vmath.matrix4() 255 | } 256 | apply_resize_mode_gui() 257 | -- Set default values for the color, depth, and stencil buffers. 258 | local clear_color_red = sys.get_config_number("render.clear_color_red", 0) 259 | local clear_color_green = sys.get_config_number("render.clear_color_green", 0) 260 | local clear_color_blue = sys.get_config_number("render.clear_color_blue", 0) 261 | local clear_color_alpha = sys.get_config_number("render.clear_color_alpha", 0) 262 | local clear_color = vmath.vector4(clear_color_red, clear_color_green, clear_color_blue, clear_color_alpha) 263 | clear_buffers = 264 | { 265 | [graphics.BUFFER_TYPE_COLOR0_BIT] = clear_color, 266 | [graphics.BUFFER_TYPE_DEPTH_BIT] = 1, 267 | [graphics.BUFFER_TYPE_STENCIL_BIT] = 0 268 | } 269 | end 270 | 271 | function update(self, dt) 272 | -- Enable writing to the depth and stencil buffers, and set the default blend function. 273 | -- Writing to the color buffer is always enabled. 274 | render.set_depth_mask(true) 275 | render.set_stencil_mask(0xff) 276 | render.set_blend_func(graphics.BLEND_FACTOR_SRC_ALPHA, graphics.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA) 277 | -- Write default values to the color, depth, and stencil buffers. 278 | render.clear(clear_buffers) 279 | -- Render each camera according to its render order. 280 | local ordered_camera_ids = get_ordered_camera_ids() 281 | for i = 1, #ordered_camera_ids do 282 | local camera_id = ordered_camera_ids[i] 283 | local camera = rendy.cameras[camera_id] 284 | -- Only render this camera if it is active. 285 | if camera.active then 286 | activate_camera(camera) 287 | -- First, render 3D objects. 288 | -- Enable depth testing and facing culling to discard non-visible fragments. 289 | -- Blending is not supported for 3D objects because Defold does not sort them from back to front, which is a requirement for proper blending. 290 | render.enable_state(graphics.STATE_DEPTH_TEST) 291 | render.enable_state(graphics.STATE_CULL_FACE) 292 | render.draw(predicates.model.object, { frustum = camera.frustum }) 293 | render.disable_state(graphics.STATE_CULL_FACE) 294 | render.disable_state(graphics.STATE_DEPTH_TEST) 295 | render.set_depth_mask(false) 296 | -- Second, render 2D objects. 297 | -- Disable depth testing because Defold sorts 2D objects by their z positions from back to front. 298 | -- Enable blending to support partially transparent objects. 299 | render.enable_state(graphics.STATE_BLEND) 300 | render.draw(predicates.tile.object, { frustum = camera.frustum }) 301 | render.draw(predicates.particle.object, { frustum = camera.frustum }) 302 | render.disable_state(graphics.STATE_BLEND) 303 | -- Render debug graphics if enabled in the game.project file. 304 | render.enable_state(graphics.STATE_BLEND) 305 | render.draw_debug3d() 306 | render.disable_state(graphics.STATE_BLEND) 307 | end 308 | end 309 | -- Render the GUI camera on top of all other cameras. 310 | -- Disable depth testing because Defold sorts 2D objects by their z positions from back to front. 311 | -- Enable blending to support partially transparent objects. 312 | -- Enable stencil testing. 313 | activate_camera(camera_gui) 314 | render.set_depth_mask(false) 315 | render.enable_state(graphics.STATE_BLEND) 316 | render.enable_state(graphics.STATE_STENCIL_TEST) 317 | render.draw(predicates.gui.object) 318 | render.draw(predicates.debug_text.object) 319 | render.disable_state(graphics.STATE_STENCIL_TEST) 320 | render.disable_state(graphics.STATE_BLEND) 321 | end 322 | 323 | function on_message(self, message_id, message, sender) 324 | -- Sent once per frame by each camera. 325 | if message_id == message_set_view_projection then 326 | set_view_projection_callback(sender, message.view) 327 | -- Sent whenever the window size changes. 328 | elseif message_id == message_window_resized then 329 | window_resized_callback() 330 | end 331 | end -------------------------------------------------------------------------------- /rendy/rendy.script: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- License 3 | -------------------------------------------------------------------------------- 4 | 5 | -- Copyright (c) 2024 White Box Dev 6 | 7 | -- This software is provided 'as-is', without any express or implied warranty. 8 | -- In no event will the authors be held liable for any damages arising from the use of this software. 9 | 10 | -- Permission is granted to anyone to use this software for any purpose, 11 | -- including commercial applications, and to alter it and redistribute it freely, 12 | -- subject to the following restrictions: 13 | 14 | -- 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. 15 | -- If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 16 | 17 | -- 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 18 | 19 | -- 3. This notice may not be removed or altered from any source distribution. 20 | 21 | -------------------------------------------------------------------------------- 22 | -- Information 23 | -------------------------------------------------------------------------------- 24 | 25 | -- GitHub: https://github.com/whiteboxdev/library-defold-rendy 26 | 27 | -------------------------------------------------------------------------------- 28 | -- Dependencies 29 | -------------------------------------------------------------------------------- 30 | 31 | local rendy = require "rendy.rendy" 32 | 33 | -------------------------------------------------------------------------------- 34 | -- Constants 35 | -------------------------------------------------------------------------------- 36 | 37 | local message_acquire_input_focus = hash("acquire_input_focus") 38 | local message_release_input_focus = hash("release_input_focus") 39 | 40 | local diagonal_scaler = 1 / math.sqrt(2) 41 | 42 | -------------------------------------------------------------------------------- 43 | -- Properties 44 | -------------------------------------------------------------------------------- 45 | 46 | -- The following properties are for configuration purposes only. 47 | -- Upon calling `rendy.create_camera()`, they will be extracted from this script. 48 | go.property("active", true) 49 | go.property("orthographic", true) 50 | go.property("resize_mode_center", true) 51 | go.property("resize_mode_expand", false) 52 | go.property("resize_mode_stretch", false) 53 | go.property("experimental_controls", false) 54 | go.property("experimental_speed", 100) 55 | go.property("render_order", 1) 56 | go.property("viewport_x", 0) 57 | go.property("viewport_y", 0) 58 | go.property("viewport_width", 960) 59 | go.property("viewport_height", 640) 60 | go.property("resolution_width", 960) 61 | go.property("resolution_height", 640) 62 | go.property("z_min", -1) 63 | go.property("z_max", 1) 64 | go.property("zoom", 1) 65 | go.property("field_of_view", 45) 66 | 67 | -------------------------------------------------------------------------------- 68 | -- Local Functions 69 | -------------------------------------------------------------------------------- 70 | 71 | -- Updates orthographic camera controls. 72 | local function update_orthographic(self, dt) 73 | if self.input.left + self.input.right ~= 0 or self.input.up + self.input.down ~= 0 then 74 | local camera_id = go.get_id() 75 | local position = go.get_position() 76 | local speed = self.input.left + self.input.right ~= 0 and self.camera.experimental_speed * diagonal_scaler or self.camera.experimental_speed 77 | local velocity = vmath.vector3((-self.input.left + self.input.right) * speed, (self.input.up - self.input.down) * speed, 0) 78 | go.set_position(position + velocity * dt) 79 | end 80 | end 81 | 82 | -- Updates perspective camera controls. 83 | local function update_perspective(self, dt) 84 | if self.input.screen_dx ~= 0 or self.input.screen_dy ~= 0 then 85 | local rotation = go.get_rotation() 86 | rotation = rotation * vmath.quat_rotation_x(self.input.screen_dy * 0.002) 87 | rotation = rotation * vmath.quat_rotation_y(-self.input.screen_dx * 0.002) 88 | go.set_rotation(rotation) 89 | local euler_x = go.get(go.get_id(), "euler.x") 90 | if euler_x < -89 then 91 | go.set(go.get_id(), "euler.x", -89) 92 | elseif euler_x > 89 then 93 | go.set(go.get_id(), "euler.x", 89) 94 | end 95 | self.input.screen_dx = 0 96 | self.input.screen_dy = 0 97 | end 98 | if self.input.left + self.input.right ~= 0 or self.input.up + self.input.down ~= 0 then 99 | local position = go.get_position() 100 | local rotation = go.get_rotation() 101 | local absolute_direction = vmath.vector3(-self.input.left + self.input.right, 0, -self.input.up + self.input.down) 102 | local relative_direction = vmath.normalize(vmath.rotate(rotation, absolute_direction)) 103 | go.set_position(position + relative_direction * self.camera.experimental_speed * dt) 104 | end 105 | end 106 | 107 | -------------------------------------------------------------------------------- 108 | -- Engine Functions 109 | -------------------------------------------------------------------------------- 110 | 111 | -- Creates the camera and initializes variables that are only relevant to the rendy.script file. 112 | function init(self) 113 | rendy.create_camera(go.get_id()) 114 | self.camera = rendy.cameras[go.get_id()] 115 | self.input = 116 | { 117 | up = 0, 118 | down = 0, 119 | left = 0, 120 | right = 0, 121 | screen_dx = 0, 122 | screen_dy = 0 123 | } 124 | end 125 | 126 | -- Destroys the camera. 127 | function final(self) 128 | rendy.destroy_camera(go.get_id()) 129 | end 130 | 131 | -- Handles camera movement if experimental controls are enabled. 132 | function update(self, dt) 133 | if self.camera.experimental_controls then 134 | if self.camera.orthographic then 135 | update_orthographic(self, dt) 136 | else 137 | update_perspective(self, dt) 138 | end 139 | end 140 | end 141 | 142 | -- Handles camera movement if experimental controls are enabled. 143 | function on_input(self, action_id, action) 144 | if not action_id then 145 | self.input.screen_dx = action.screen_dx 146 | self.input.screen_dy = action.screen_dy 147 | end 148 | if action.pressed then 149 | if action_id == hash("key_w") then 150 | self.input.up = 1 151 | elseif action_id == hash("key_a") then 152 | self.input.left = 1 153 | elseif action_id == hash("key_s") then 154 | self.input.down = 1 155 | elseif action_id == hash("key_d") then 156 | self.input.right = 1 157 | end 158 | elseif action.released then 159 | if action_id == hash("key_w") then 160 | self.input.up = 0 161 | elseif action_id == hash("key_a") then 162 | self.input.left = 0 163 | elseif action_id == hash("key_s") then 164 | self.input.down = 0 165 | elseif action_id == hash("key_d") then 166 | self.input.right = 0 167 | end 168 | end 169 | end 170 | --------------------------------------------------------------------------------