postprocess.render_script 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. -- Copyright 2020-2025 The Defold Foundation
  2. -- Copyright 2014-2020 King
  3. -- Copyright 2009-2014 Ragnar Svensson, Christian Murray
  4. -- Licensed under the Defold License version 1.0 (the "License"); you may not use
  5. -- this file except in compliance with the License.
  6. --
  7. -- You may obtain a copy of the License, together with FAQs at
  8. -- https://www.defold.com/license
  9. --
  10. -- Unless required by applicable law or agreed to in writing, software distributed
  11. -- under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
  12. -- CONDITIONS OF ANY KIND, either express or implied. See the License for the
  13. -- specific language governing permissions and limitations under the License.
  14. --
  15. -- message constants
  16. --
  17. local MSG_CLEAR_COLOR = hash("clear_color")
  18. local MSG_WINDOW_RESIZED = hash("window_resized")
  19. local MSG_SET_VIEW_PROJ = hash("set_view_projection")
  20. local MSG_SET_CAMERA_PROJ = hash("use_camera_projection")
  21. local MSG_USE_STRETCH_PROJ = hash("use_stretch_projection")
  22. local MSG_USE_FIXED_PROJ = hash("use_fixed_projection")
  23. local MSG_USE_FIXED_FIT_PROJ = hash("use_fixed_fit_projection")
  24. local DEFAULT_NEAR = -1
  25. local DEFAULT_FAR = 1
  26. local DEFAULT_ZOOM = 1
  27. --
  28. -- projection that centers content with maintained aspect ratio and optional zoom
  29. --
  30. local function get_fixed_projection(camera, state)
  31. camera.zoom = camera.zoom or DEFAULT_ZOOM
  32. local projected_width = state.window_width / camera.zoom
  33. local projected_height = state.window_height / camera.zoom
  34. local left = -(projected_width - state.width) / 2
  35. local bottom = -(projected_height - state.height) / 2
  36. local right = left + projected_width
  37. local top = bottom + projected_height
  38. return vmath.matrix4_orthographic(left, right, bottom, top, camera.near, camera.far)
  39. end
  40. --
  41. -- projection that centers and fits content with maintained aspect ratio
  42. --
  43. local function get_fixed_fit_projection(camera, state)
  44. camera.zoom = math.min(state.window_width / state.width, state.window_height / state.height)
  45. return get_fixed_projection(camera, state)
  46. end
  47. --
  48. -- projection that stretches content
  49. --
  50. local function get_stretch_projection(camera, state)
  51. return vmath.matrix4_orthographic(0, state.width, 0, state.height, camera.near, camera.far)
  52. end
  53. --
  54. -- projection for gui
  55. --
  56. local function get_gui_projection(camera, state)
  57. return vmath.matrix4_orthographic(0, state.window_width, 0, state.window_height, camera.near, camera.far)
  58. end
  59. local function update_clear_color(state, color)
  60. if color then
  61. state.clear_buffers[graphics.BUFFER_TYPE_COLOR0_BIT] = color
  62. end
  63. end
  64. local function update_camera(camera, state)
  65. if camera.projection_fn then
  66. camera.proj = camera.projection_fn(camera, state)
  67. camera.options.frustum = camera.proj * camera.view
  68. end
  69. end
  70. local function update_state(state)
  71. state.window_width = render.get_window_width()
  72. state.window_height = render.get_window_height()
  73. state.valid = state.window_width > 0 and state.window_height > 0
  74. if not state.valid then
  75. return false
  76. end
  77. -- Make sure state updated only once when resize window
  78. if state.window_width == state.prev_window_width and state.window_height == state.prev_window_height then
  79. return true
  80. end
  81. state.prev_window_width = state.window_width
  82. state.prev_window_height = state.window_height
  83. state.width = render.get_width()
  84. state.height = render.get_height()
  85. for _, camera in pairs(state.cameras) do
  86. update_camera(camera, state)
  87. end
  88. return true
  89. end
  90. local function init_camera(camera, projection_fn, near, far, zoom)
  91. camera.view = vmath.matrix4()
  92. camera.near = near == nil and DEFAULT_NEAR or near
  93. camera.far = far == nil and DEFAULT_FAR or far
  94. camera.zoom = zoom == nil and DEFAULT_ZOOM or zoom
  95. camera.projection_fn = projection_fn
  96. end
  97. local function create_predicates(...)
  98. local arg = {...}
  99. local predicates = {}
  100. for _, predicate_name in pairs(arg) do
  101. predicates[predicate_name] = render.predicate({predicate_name})
  102. end
  103. return predicates
  104. end
  105. local function create_camera(state, name, is_main_camera)
  106. local camera = {}
  107. camera.options = {}
  108. state.cameras[name] = camera
  109. if is_main_camera then
  110. state.main_camera = camera
  111. end
  112. return camera
  113. end
  114. local function create_state()
  115. local state = {}
  116. local color = vmath.vector4(0, 0, 0, 0)
  117. color.x = sys.get_config_number("render.clear_color_red", 0)
  118. color.y = sys.get_config_number("render.clear_color_green", 0)
  119. color.z = sys.get_config_number("render.clear_color_blue", 0)
  120. color.w = sys.get_config_number("render.clear_color_alpha", 0)
  121. state.clear_buffers = {
  122. [graphics.BUFFER_TYPE_COLOR0_BIT] = color,
  123. [graphics.BUFFER_TYPE_DEPTH_BIT] = 1,
  124. [graphics.BUFFER_TYPE_STENCIL_BIT] = 0
  125. }
  126. state.cameras = {}
  127. return state
  128. end
  129. local function set_camera_world(state)
  130. local camera_components = camera.get_cameras()
  131. -- This will set the last enabled camera from the stack of camera components
  132. if #camera_components > 0 then
  133. for i = #camera_components, 1, -1 do
  134. if camera.get_enabled(camera_components[i]) then
  135. local camera_component = state.cameras.camera_component
  136. camera_component.camera = camera_components[i]
  137. render.set_camera(camera_component.camera, { use_frustum = true })
  138. -- The frustum will be overridden by the render.set_camera call,
  139. -- so we don't need to return anything here other than an empty table.
  140. return camera_component.options
  141. end
  142. end
  143. end
  144. -- If no active camera was found, we use the default main "camera world" camera
  145. local camera_world = state.cameras.camera_world
  146. render.set_view(camera_world.view)
  147. render.set_projection(camera_world.proj)
  148. return camera_world.options
  149. end
  150. local function reset_camera_world(state)
  151. -- unbind the camera if a camera component is used
  152. if state.cameras.camera_component.camera then
  153. state.cameras.camera_component.camera = nil
  154. render.set_camera()
  155. end
  156. end
  157. local function create_postprocess_rt(self, width, height)
  158. local color_params = {
  159. format = graphics.TEXTURE_FORMAT_RGBA,
  160. width = width,
  161. height = height,
  162. min_filter = render.FILTER_LINEAR,
  163. mag_filter = render.FILTER_LINEAR,
  164. u_wrap = render.WRAP_CLAMP_TO_EDGE,
  165. v_wrap = render.WRAP_CLAMP_TO_EDGE
  166. }
  167. local depth_params = {
  168. format = graphics.TEXTURE_FORMAT_DEPTH,
  169. width = width,
  170. height = height,
  171. }
  172. self.postprocess_rt = render.render_target("postprocess_rt", { [render.BUFFER_COLOR_BIT] = color_params, [render.BUFFER_DEPTH_BIT] = depth_params } )
  173. self.postprocess_rt_width = width
  174. self.postprocess_rt_height = height
  175. end
  176. local function update_postprocess_rt(self)
  177. local w = render.get_window_width()
  178. local h = render.get_window_height()
  179. -- keep render target if size is the same
  180. if self.postprocess_rt_width == w and self.postprocess_rt_height == h then
  181. return
  182. end
  183. render.delete_render_target(self.postprocess_rt)
  184. create_postprocess_rt(self, w, h)
  185. end
  186. function init(self)
  187. -- create the postprocess predicate and all of the default predicates
  188. self.predicates = create_predicates("postprocess", "tile", "gui", "particle", "model", "debug_text")
  189. -- default is stretch projection. copy from builtins and change for different projection
  190. -- or send a message to the render script to change projection:
  191. -- msg.post("@render:", "use_stretch_projection", { near = -1, far = 1 })
  192. -- msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = 2 })
  193. -- msg.post("@render:", "use_fixed_fit_projection", { near = -1, far = 1 })
  194. create_postprocess_rt(self, render.get_window_width(), render.get_window_height())
  195. local state = create_state()
  196. self.state = state
  197. local camera_world = create_camera(state, "camera_world", true)
  198. init_camera(camera_world, get_stretch_projection)
  199. local camera_gui = create_camera(state, "camera_gui")
  200. init_camera(camera_gui, get_gui_projection)
  201. -- Create a special camera that wraps camera components (if they exist)
  202. -- It will take precedence over any other camera, and not change from messages
  203. local camera_component = create_camera(state, "camera_component")
  204. update_state(state)
  205. end
  206. function update(self)
  207. update_postprocess_rt(self)
  208. local state = self.state
  209. if not state.valid then
  210. if not update_state(state) then
  211. return
  212. end
  213. end
  214. -- enable postprecssing render target
  215. -- subsequent draw operations will be done to the render target
  216. --
  217. render.set_render_target(self.postprocess_rt)
  218. local predicates = self.predicates
  219. -- clear screen buffers
  220. --
  221. -- turn on depth_mask before `render.clear()` to clear it as well
  222. render.set_depth_mask(true)
  223. render.set_stencil_mask(0xff)
  224. render.clear(state.clear_buffers)
  225. -- setup camera view and projection
  226. --
  227. local draw_options_world = set_camera_world(state)
  228. render.set_viewport(0, 0, state.window_width, state.window_height)
  229. -- set states used for all the world predicates
  230. render.set_blend_func(graphics.BLEND_FACTOR_SRC_ALPHA, graphics.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA)
  231. render.enable_state(graphics.STATE_DEPTH_TEST)
  232. -- render `model` predicate for default 3D material
  233. --
  234. render.enable_state(graphics.STATE_CULL_FACE)
  235. render.draw(predicates.model, draw_options_world)
  236. render.set_depth_mask(false)
  237. render.disable_state(graphics.STATE_CULL_FACE)
  238. -- render the other components: sprites, tilemaps, particles etc
  239. --
  240. render.enable_state(graphics.STATE_BLEND)
  241. render.draw(predicates.tile, draw_options_world)
  242. render.draw(predicates.particle, draw_options_world)
  243. render.disable_state(graphics.STATE_DEPTH_TEST)
  244. render.draw_debug3d()
  245. reset_camera_world(state)
  246. -- render GUI
  247. --
  248. local camera_gui = state.cameras.camera_gui
  249. render.set_view(camera_gui.view)
  250. render.set_projection(camera_gui.proj)
  251. render.enable_state(graphics.STATE_STENCIL_TEST)
  252. render.draw(predicates.gui, camera_gui.options)
  253. render.draw(predicates.debug_text, camera_gui.options)
  254. render.disable_state(graphics.STATE_STENCIL_TEST)
  255. render.disable_state(graphics.STATE_BLEND)
  256. -- revert to the default render target
  257. --
  258. render.set_render_target(render.RENDER_TARGET_DEFAULT)
  259. -- render post processing render target to quad with predicate 'postprocess'
  260. --
  261. render.set_view(vmath.matrix4())
  262. render.set_projection(vmath.matrix4())
  263. render.enable_texture(0, self.postprocess_rt, render.BUFFER_COLOR_BIT)
  264. render.draw(predicates.postprocess)
  265. render.disable_texture(0)
  266. end
  267. function on_message(self, message_id, message)
  268. local state = self.state
  269. local camera = state.main_camera
  270. if message_id == MSG_CLEAR_COLOR then
  271. update_clear_color(state, message.color)
  272. elseif message_id == MSG_WINDOW_RESIZED then
  273. update_state(state)
  274. elseif message_id == MSG_SET_VIEW_PROJ then
  275. camera.view = message.view
  276. self.camera_projection = message.projection or vmath.matrix4()
  277. update_camera(camera, state)
  278. elseif message_id == MSG_SET_CAMERA_PROJ then
  279. camera.projection_fn = function() return self.camera_projection end
  280. elseif message_id == MSG_USE_STRETCH_PROJ then
  281. init_camera(camera, get_stretch_projection, message.near, message.far)
  282. update_camera(camera, state)
  283. elseif message_id == MSG_USE_FIXED_PROJ then
  284. init_camera(camera, get_fixed_projection, message.near, message.far, message.zoom)
  285. update_camera(camera, state)
  286. elseif message_id == MSG_USE_FIXED_FIT_PROJ then
  287. init_camera(camera, get_fixed_fit_projection, message.near, message.far)
  288. update_camera(camera, state)
  289. end
  290. end