-- Copyright 2020-2025 The Defold Foundation -- Copyright 2014-2020 King -- Copyright 2009-2014 Ragnar Svensson, Christian Murray -- Licensed under the Defold License version 1.0 (the "License"); you may not use -- this file except in compliance with the License. -- -- You may obtain a copy of the License, together with FAQs at -- https://www.defold.com/license -- -- Unless required by applicable law or agreed to in writing, software distributed -- under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -- CONDITIONS OF ANY KIND, either express or implied. See the License for the -- specific language governing permissions and limitations under the License. -- -- message constants -- local MSG_CLEAR_COLOR = hash("clear_color") local MSG_WINDOW_RESIZED = hash("window_resized") local MSG_SET_VIEW_PROJ = hash("set_view_projection") local MSG_SET_CAMERA_PROJ = hash("use_camera_projection") local MSG_USE_STRETCH_PROJ = hash("use_stretch_projection") local MSG_USE_FIXED_PROJ = hash("use_fixed_projection") local MSG_USE_FIXED_FIT_PROJ = hash("use_fixed_fit_projection") local DEFAULT_NEAR = -1 local DEFAULT_FAR = 1 local DEFAULT_ZOOM = 1 -- -- projection that centers content with maintained aspect ratio and optional zoom -- local function get_fixed_projection(camera, state) camera.zoom = camera.zoom or DEFAULT_ZOOM local projected_width = state.window_width / camera.zoom local projected_height = state.window_height / camera.zoom local left = -(projected_width - state.width) / 2 local bottom = -(projected_height - state.height) / 2 local right = left + projected_width local top = bottom + projected_height return vmath.matrix4_orthographic(left, right, bottom, top, camera.near, camera.far) end -- -- projection that centers and fits content with maintained aspect ratio -- local function get_fixed_fit_projection(camera, state) camera.zoom = math.min(state.window_width / state.width, state.window_height / state.height) return get_fixed_projection(camera, state) end -- -- projection that stretches content -- local function get_stretch_projection(camera, state) return vmath.matrix4_orthographic(0, state.width, 0, state.height, camera.near, camera.far) end -- -- projection for gui -- local function get_gui_projection(camera, state) return vmath.matrix4_orthographic(0, state.window_width, 0, state.window_height, camera.near, camera.far) end local function update_clear_color(state, color) if color then state.clear_buffers[graphics.BUFFER_TYPE_COLOR0_BIT] = color end end local function update_camera(camera, state) if camera.projection_fn then camera.proj = camera.projection_fn(camera, state) camera.options.frustum = camera.proj * camera.view end end local function update_state(state) state.window_width = render.get_window_width() state.window_height = render.get_window_height() state.valid = state.window_width > 0 and state.window_height > 0 if not state.valid then return false end -- Make sure state updated only once when resize window if state.window_width == state.prev_window_width and state.window_height == state.prev_window_height then return true end state.prev_window_width = state.window_width state.prev_window_height = state.window_height state.width = render.get_width() state.height = render.get_height() for _, camera in pairs(state.cameras) do update_camera(camera, state) end return true end local function init_camera(camera, projection_fn, near, far, zoom) camera.view = vmath.matrix4() camera.near = near == nil and DEFAULT_NEAR or near camera.far = far == nil and DEFAULT_FAR or far camera.zoom = zoom == nil and DEFAULT_ZOOM or zoom camera.projection_fn = projection_fn end local function create_predicates(...) local arg = {...} local predicates = {} for _, predicate_name in pairs(arg) do predicates[predicate_name] = render.predicate({predicate_name}) end return predicates end local function create_camera(state, name, is_main_camera) local camera = {} camera.options = {} state.cameras[name] = camera if is_main_camera then state.main_camera = camera end return camera end local function create_state() local state = {} local color = vmath.vector4(0, 0, 0, 0) color.x = sys.get_config_number("render.clear_color_red", 0) color.y = sys.get_config_number("render.clear_color_green", 0) color.z = sys.get_config_number("render.clear_color_blue", 0) color.w = sys.get_config_number("render.clear_color_alpha", 0) state.clear_buffers = { [graphics.BUFFER_TYPE_COLOR0_BIT] = color, [graphics.BUFFER_TYPE_DEPTH_BIT] = 1, [graphics.BUFFER_TYPE_STENCIL_BIT] = 0 } state.cameras = {} return state end local function set_camera_world(state) local camera_components = camera.get_cameras() -- This will set the last enabled camera from the stack of camera components if #camera_components > 0 then for i = #camera_components, 1, -1 do if camera.get_enabled(camera_components[i]) then local camera_component = state.cameras.camera_component camera_component.camera = camera_components[i] render.set_camera(camera_component.camera, { use_frustum = true }) -- The frustum will be overridden by the render.set_camera call, -- so we don't need to return anything here other than an empty table. return camera_component.options end end end -- If no active camera was found, we use the default main "camera world" camera local camera_world = state.cameras.camera_world render.set_view(camera_world.view) render.set_projection(camera_world.proj) return camera_world.options end local function reset_camera_world(state) -- unbind the camera if a camera component is used if state.cameras.camera_component.camera then state.cameras.camera_component.camera = nil render.set_camera() end end function init(self) self.predicates = create_predicates("tile", "gui", "particle", "model", "skybox", "debug_text") -- default is stretch projection. copy from builtins and change for different projection -- or send a message to the render script to change projection: -- msg.post("@render:", "use_stretch_projection", { near = -1, far = 1 }) -- msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = 2 }) -- msg.post("@render:", "use_fixed_fit_projection", { near = -1, far = 1 }) local state = create_state() self.state = state local camera_world = create_camera(state, "camera_world", true) init_camera(camera_world, get_stretch_projection) local camera_gui = create_camera(state, "camera_gui") init_camera(camera_gui, get_gui_projection) -- Create a special camera that wraps camera components (if they exist) -- It will take precedence over any other camera, and not change from messages local camera_component = create_camera(state, "camera_component") update_state(state) end function update(self) local state = self.state if not state.valid then if not update_state(state) then return end end local predicates = self.predicates -- clear screen buffers -- -- turn on depth_mask before `render.clear()` to clear it as well render.set_depth_mask(true) render.set_stencil_mask(0xff) render.clear(state.clear_buffers) -- setup camera view and projection -- local draw_options_world = set_camera_world(state) render.set_viewport(0, 0, state.window_width, state.window_height) -- set states used for all the world predicates render.set_blend_func(graphics.BLEND_FACTOR_SRC_ALPHA, graphics.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA) render.enable_state(graphics.STATE_DEPTH_TEST) -- render `model` predicate for default 3D material -- render.enable_state(graphics.STATE_CULL_FACE) render.draw(predicates.model, draw_options_world) render.set_depth_mask(false) render.disable_state(graphics.STATE_CULL_FACE) -- render `skybox` -- https://www.ogldev.org/www/tutorial25/tutorial25.html -- render.enable_state(graphics.STATE_CULL_FACE) -- Usually we want to cull the triangles that are facing away from the camera -- However in the case of a skybox the camera is placed inside of a box/sphere -- so we want to see their front, rather than their back render.set_cull_face(graphics.FACE_TYPE_FRONT) -- By default, we tell OpenGL that an incoming fragment wins the depth test if -- its Z value is less than the stored one. However, in the case of a skybox -- the Z value is always the far Z. The far Z is clipped when the depth test -- function is set to "less than". To make it part of the scene we change the -- depth function to "less than or equal". render.set_depth_func(graphics.COMPARE_FUNC_LEQUAL) render.draw(predicates.skybox, draw_options_world) render.set_depth_mask(false) render.set_depth_func(graphics.COMPARE_FUNC_LESS) render.set_cull_face(graphics.FACE_TYPE_BACK) render.disable_state(graphics.STATE_CULL_FACE) -- render the other components: sprites, tilemaps, particles etc -- render.enable_state(graphics.STATE_BLEND) render.draw(predicates.tile, draw_options_world) render.draw(predicates.particle, draw_options_world) render.disable_state(graphics.STATE_DEPTH_TEST) render.draw_debug3d() reset_camera_world(state) -- render GUI -- local camera_gui = state.cameras.camera_gui render.set_view(camera_gui.view) render.set_projection(camera_gui.proj) render.enable_state(graphics.STATE_STENCIL_TEST) render.draw(predicates.gui, camera_gui.options) render.draw(predicates.debug_text, camera_gui.options) render.disable_state(graphics.STATE_STENCIL_TEST) render.disable_state(graphics.STATE_BLEND) end function on_message(self, message_id, message) local state = self.state local camera = state.main_camera if message_id == MSG_CLEAR_COLOR then update_clear_color(state, message.color) elseif message_id == MSG_WINDOW_RESIZED then update_state(state) elseif message_id == MSG_SET_VIEW_PROJ then camera.view = message.view self.camera_projection = message.projection or vmath.matrix4() update_camera(camera, state) elseif message_id == MSG_SET_CAMERA_PROJ then camera.projection_fn = function() return self.camera_projection end elseif message_id == MSG_USE_STRETCH_PROJ then init_camera(camera, get_stretch_projection, message.near, message.far) update_camera(camera, state) elseif message_id == MSG_USE_FIXED_PROJ then init_camera(camera, get_fixed_projection, message.near, message.far, message.zoom) update_camera(camera, state) elseif message_id == MSG_USE_FIXED_FIT_PROJ then init_camera(camera, get_fixed_fit_projection, message.near, message.far) update_camera(camera, state) end end