Browse Source

Added postprocess example

Björn Ritzl 1 tháng trước cách đây
mục cha
commit
73e81e98a5
31 tập tin đã thay đổi với 970 bổ sung0 xóa
  1. 18 0
      render/post_processing/all.texture_profiles
  2. BIN
      render/post_processing/assets/SourceSansPro-Semibold.ttf
  3. 47 0
      render/post_processing/assets/materials/model_world.material
  4. BIN
      render/post_processing/assets/models/Knight.glb
  5. BIN
      render/post_processing/assets/models/dungeon_texture.png
  6. BIN
      render/post_processing/assets/models/floor_tile_large.gltf.glb
  7. BIN
      render/post_processing/assets/models/knight_texture.png
  8. BIN
      render/post_processing/assets/models/wall.gltf.glb
  9. 4 0
      render/post_processing/assets/text32.font
  10. 104 0
      render/post_processing/example.md
  11. 137 0
      render/post_processing/example/example.collection
  12. 27 0
      render/post_processing/example/instructions.gui
  13. 18 0
      render/post_processing/example/materials/invert.fp
  14. 15 0
      render/post_processing/example/materials/invert.material
  15. 18 0
      render/post_processing/example/materials/invert.vp
  16. 14 0
      render/post_processing/example/materials/passthrough.fp
  17. 8 0
      render/post_processing/example/materials/passthrough.material
  18. 18 0
      render/post_processing/example/materials/passthrough.vp
  19. 24 0
      render/post_processing/example/materials/scanlines.fp
  20. 14 0
      render/post_processing/example/materials/scanlines.material
  21. 17 0
      render/post_processing/example/materials/scanlines.vp
  22. 49 0
      render/post_processing/example/orbit_camera.script
  23. 20 0
      render/post_processing/example/player.script
  24. 1 0
      render/post_processing/example/postprocess.render
  25. 331 0
      render/post_processing/example/postprocess.render_script
  26. 20 0
      render/post_processing/example/postprocess.script
  27. 66 0
      render/post_processing/game.project
  28. BIN
      render/post_processing/invert_material.png
  29. BIN
      render/post_processing/material_properties.png
  30. BIN
      render/post_processing/postprocess_thumb.png
  31. BIN
      render/post_processing/quad.png

+ 18 - 0
render/post_processing/all.texture_profiles

@@ -0,0 +1,18 @@
+path_settings {
+  path: "**"
+  profile: "Default"
+}
+profiles {
+  name: "Default"
+  platforms {
+    os: OS_ID_GENERIC
+    formats {
+      format: TEXTURE_FORMAT_RGBA
+      compression_level: BEST
+      compression_type: COMPRESSION_TYPE_DEFAULT
+    }
+    mipmaps: false
+    max_texture_size: 0
+    premultiply_alpha: true
+  }
+}

BIN
render/post_processing/assets/SourceSansPro-Semibold.ttf


+ 47 - 0
render/post_processing/assets/materials/model_world.material

@@ -0,0 +1,47 @@
+name: "model"
+tags: "model"
+vertex_program: "/builtins/materials/model.vp"
+fragment_program: "/builtins/materials/model.fp"
+vertex_constants {
+  name: "mtx_worldview"
+  type: CONSTANT_TYPE_WORLDVIEW
+}
+vertex_constants {
+  name: "mtx_view"
+  type: CONSTANT_TYPE_VIEW
+}
+vertex_constants {
+  name: "mtx_proj"
+  type: CONSTANT_TYPE_PROJECTION
+}
+vertex_constants {
+  name: "mtx_normal"
+  type: CONSTANT_TYPE_NORMAL
+}
+vertex_constants {
+  name: "light"
+  type: CONSTANT_TYPE_USER
+  value {
+    x: 1.0
+    y: 1.0
+    z: 10.0
+    w: 1.0
+  }
+}
+fragment_constants {
+  name: "tint"
+  type: CONSTANT_TYPE_USER
+  value {
+    x: 1.0
+    y: 1.0
+    z: 1.0
+    w: 1.0
+  }
+}
+samplers {
+  name: "tex0"
+  wrap_u: WRAP_MODE_CLAMP_TO_EDGE
+  wrap_v: WRAP_MODE_CLAMP_TO_EDGE
+  filter_min: FILTER_MODE_MIN_LINEAR
+  filter_mag: FILTER_MODE_MAG_LINEAR
+}

BIN
render/post_processing/assets/models/Knight.glb


BIN
render/post_processing/assets/models/dungeon_texture.png


BIN
render/post_processing/assets/models/floor_tile_large.gltf.glb


BIN
render/post_processing/assets/models/knight_texture.png


BIN
render/post_processing/assets/models/wall.gltf.glb


+ 4 - 0
render/post_processing/assets/text32.font

@@ -0,0 +1,4 @@
+font: "/assets/SourceSansPro-Semibold.ttf"
+material: "/builtins/fonts/font.material"
+size: 32
+characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"

+ 104 - 0
render/post_processing/example.md

@@ -0,0 +1,104 @@
+---
+tags: render
+title: Post-processing
+brief: This example shows how to apply a post-processing effect by drawing to a render target and then to a fullscreen quad using a post processing shader.
+author: Defold Foundation
+scripts: postprocess.script, invert.fp, invert.vp
+thumbnail: postprocess_thumb.png
+---
+
+The basic principle of a full screen post processing effect is to first draw the entire game to a render target, then draw this render target to a full screen quad using a post processing shader to apply some kind of effect. This example shows a color invert effect and a CRT scanline effect.
+
+The setup in this example consists of a custom render script and a game object containing a model component with a fullscreen quad (ie rectangle).
+
+![quad](quad.png)
+
+The model uses a material with a render predicate/tag named `postprocess`. The material uses a basic shader program in `invert.fp` to invert the color of anything drawn with the material.
+
+![invert_material](invert_material.png)
+
+The render script is a copy of the default render script with added code to create a fullscreen render target. The render target has a color and depth buffer, and it will be resized if the screen resolution changes:
+
+```lua
+local function create_postprocess_rt(self, width, height)
+    local color_params = {
+        format = graphics.TEXTURE_FORMAT_RGBA,
+        width = width,
+        height = height,
+        min_filter = render.FILTER_LINEAR,
+        mag_filter = render.FILTER_LINEAR,
+        u_wrap = render.WRAP_CLAMP_TO_EDGE,
+        v_wrap = render.WRAP_CLAMP_TO_EDGE
+    }
+    local depth_params = {
+        format = graphics.TEXTURE_FORMAT_DEPTH,
+        width = width,
+        height = height,
+    }
+    self.postprocess_rt =  render.render_target("postprocess_rt", { [render.BUFFER_COLOR_BIT] = color_params, [render.BUFFER_DEPTH_BIT] = depth_params } )
+    self.postprocess_rt_width = width
+    self.postprocess_rt_height = height
+end
+
+local function update_postprocess_rt(self)
+    local w = render.get_window_width()
+    local h = render.get_window_height()
+    
+    -- keep render target if size is the same
+    if self.postprocess_rt_width == w and self.postprocess_rt_height == h then
+        return
+    end
+
+    render.delete_render_target(self.postprocess_rt)
+    create_postprocess_rt(self, w, h)
+end
+
+function init(self)
+    -- create the postprocess predicate and all of the default predicates
+    self.predicates = create_predicates("postprocess", "tile", "gui", "particle", "model", "debug_text")
+
+    create_postprocess_rt(self, render.get_window_width(), render.get_window_height())
+end
+
+function update(self)
+    update_postprocess_rt(self)
+end
+```
+
+The render script is additionally modified so that all content is drawn to the render target instead of directly to the screen. In a separate step at the end, the render target is used as a texture and drawn to the fullscreen quad with the `postprocess` predicate using the post processing shader assigned to the model quad:
+
+
+```lua
+function update(self)
+    update_postprocess_rt(self)
+
+    -- enable postprecssing render target
+    -- subsequent draw operations will be done to the render target
+    --
+    render.set_render_target(self.postprocess_rt)
+
+    -- note: some render code removed from this snippet to make it readable
+    render.draw(predicates.model, draw_options_world)
+    render.draw(predicates.tile, draw_options_world)
+    render.draw(predicates.particle, draw_options_world)
+    render.draw(predicates.gui, camera_gui.options)
+    render.draw(predicates.debug_text, camera_gui.options)
+
+    -- revert to the default render target
+    --
+    render.set_render_target(render.RENDER_TARGET_DEFAULT)
+
+    -- render post processing render target to quad with predicate 'postprocess'
+    --
+    render.set_view(vmath.matrix4())
+    render.set_projection(vmath.matrix4())
+    render.enable_texture(0, self.postprocess_rt, render.BUFFER_COLOR_BIT)
+    render.draw(predicates.postprocess)
+    render.disable_texture(0)
+end
+```
+
+Additionally the example shows in `postprocess.script` how to change material using material resource properties at runtime.
+
+![material_properties](material_properties.png)
+

+ 137 - 0
render/post_processing/example/example.collection

@@ -0,0 +1,137 @@
+name: "example"
+scale_along_z: 0
+embedded_instances {
+  id: "floor"
+  data: "embedded_components {\n"
+  "  id: \"model\"\n"
+  "  type: \"model\"\n"
+  "  data: \"mesh: \\\"/assets/models/floor_tile_large.gltf.glb\\\"\\n"
+  "name: \\\"{{NAME}}\\\"\\n"
+  "materials {\\n"
+  "  name: \\\"texture\\\"\\n"
+  "  material: \\\"/builtins/materials/model.material\\\"\\n"
+  "  textures {\\n"
+  "    sampler: \\\"tex0\\\"\\n"
+  "    texture: \\\"/assets/models/dungeon_texture.png\\\"\\n"
+  "  }\\n"
+  "}\\n"
+  "\"\n"
+  "}\n"
+  ""
+}
+embedded_instances {
+  id: "wall"
+  data: "embedded_components {\n"
+  "  id: \"model\"\n"
+  "  type: \"model\"\n"
+  "  data: \"mesh: \\\"/assets/models/wall.gltf.glb\\\"\\n"
+  "name: \\\"{{NAME}}\\\"\\n"
+  "materials {\\n"
+  "  name: \\\"texture\\\"\\n"
+  "  material: \\\"/builtins/materials/model.material\\\"\\n"
+  "  textures {\\n"
+  "    sampler: \\\"tex0\\\"\\n"
+  "    texture: \\\"/assets/models/dungeon_texture.png\\\"\\n"
+  "  }\\n"
+  "}\\n"
+  "\"\n"
+  "}\n"
+  ""
+  position {
+    z: -2.0
+  }
+}
+embedded_instances {
+  id: "player"
+  children: "camera"
+  data: "components {\n"
+  "  id: \"player\"\n"
+  "  component: \"/example/player.script\"\n"
+  "}\n"
+  "embedded_components {\n"
+  "  id: \"model\"\n"
+  "  type: \"model\"\n"
+  "  data: \"mesh: \\\"/assets/models/Knight.glb\\\"\\n"
+  "skeleton: \\\"/assets/models/Knight.glb\\\"\\n"
+  "animations: \\\"/assets/models/Knight.glb\\\"\\n"
+  "default_animation: \\\"Block_Attack\\\"\\n"
+  "name: \\\"{{NAME}}\\\"\\n"
+  "materials {\\n"
+  "  name: \\\"knight_texture\\\"\\n"
+  "  material: \\\"/assets/materials/model_world.material\\\"\\n"
+  "  textures {\\n"
+  "    sampler: \\\"tex0\\\"\\n"
+  "    texture: \\\"/assets/models/knight_texture.png\\\"\\n"
+  "  }\\n"
+  "}\\n"
+  "materials {\\n"
+  "  name: \\\"knight_texture_no_skin\\\"\\n"
+  "  material: \\\"/assets/materials/model_world.material\\\"\\n"
+  "  textures {\\n"
+  "    sampler: \\\"tex0\\\"\\n"
+  "    texture: \\\"/assets/models/knight_texture.png\\\"\\n"
+  "  }\\n"
+  "}\\n"
+  "\"\n"
+  "}\n"
+  ""
+}
+embedded_instances {
+  id: "camera"
+  data: "components {\n"
+  "  id: \"orbit_camera\"\n"
+  "  component: \"/example/orbit_camera.script\"\n"
+  "  properties {\n"
+  "    id: \"zoom\"\n"
+  "    value: \"13.0\"\n"
+  "    type: PROPERTY_TYPE_NUMBER\n"
+  "  }\n"
+  "}\n"
+  "embedded_components {\n"
+  "  id: \"camera\"\n"
+  "  type: \"camera\"\n"
+  "  data: \"aspect_ratio: 1.0\\n"
+  "fov: 0.7854\\n"
+  "near_z: 0.1\\n"
+  "far_z: 1000.0\\n"
+  "auto_aspect_ratio: 1\\n"
+  "\"\n"
+  "}\n"
+  ""
+  position {
+    y: 5.0
+    z: 8.0
+  }
+  rotation {
+    x: -0.21643962
+    w: 0.976296
+  }
+}
+embedded_instances {
+  id: "postprocess"
+  data: "components {\n"
+  "  id: \"postprocess\"\n"
+  "  component: \"/example/postprocess.script\"\n"
+  "}\n"
+  "embedded_components {\n"
+  "  id: \"quad\"\n"
+  "  type: \"model\"\n"
+  "  data: \"mesh: \\\"/builtins/assets/meshes/quad_2x2.dae\\\"\\n"
+  "name: \\\"{{NAME}}\\\"\\n"
+  "materials {\\n"
+  "  name: \\\"default\\\"\\n"
+  "  material: \\\"/example/materials/passthrough.material\\\"\\n"
+  "}\\n"
+  "create_go_bones: false\\n"
+  "\"\n"
+  "}\n"
+  ""
+}
+embedded_instances {
+  id: "instructions"
+  data: "components {\n"
+  "  id: \"instructions\"\n"
+  "  component: \"/example/instructions.gui\"\n"
+  "}\n"
+  ""
+}

+ 27 - 0
render/post_processing/example/instructions.gui

@@ -0,0 +1,27 @@
+fonts {
+  name: "text"
+  font: "/assets/text32.font"
+}
+nodes {
+  position {
+    x: 360.0
+    y: 5.0
+  }
+  size {
+    x: 720.0
+    y: 60.0
+  }
+  color {
+    x: 0.0
+    y: 0.0
+    z: 0.0
+  }
+  type: TYPE_TEXT
+  text: "1 = Invert, 2 = Scanlines, 3 = Passthrough"
+  font: "text"
+  id: "text"
+  pivot: PIVOT_S
+  inherit_alpha: true
+}
+material: "/builtins/materials/gui.material"
+adjust_reference: ADJUST_REFERENCE_PARENT

+ 18 - 0
render/post_processing/example/materials/invert.fp

@@ -0,0 +1,18 @@
+#version 140
+
+in mediump vec2 var_texcoord0;
+
+out vec4 out_fragColor;
+
+uniform mediump sampler2D tex0;
+
+void main()
+{
+    vec4 color = texture(tex0, var_texcoord0.xy);
+    color.r = 1.0 - color.r;
+    color.g = 1.0 - color.g;
+    color.b = 1.0 - color.b;
+        
+    out_fragColor = vec4(color.rgb,1.0);
+}
+

+ 15 - 0
render/post_processing/example/materials/invert.material

@@ -0,0 +1,15 @@
+name: "invert"
+tags: "postprocess"
+vertex_program: "/example/materials/invert.vp"
+fragment_program: "/example/materials/invert.fp"
+vertex_constants {
+  name: "view_proj"
+  type: CONSTANT_TYPE_VIEWPROJ
+}
+samplers {
+  name: "tex0"
+  wrap_u: WRAP_MODE_CLAMP_TO_EDGE
+  wrap_v: WRAP_MODE_CLAMP_TO_EDGE
+  filter_min: FILTER_MODE_MIN_LINEAR
+  filter_mag: FILTER_MODE_MAG_LINEAR
+}

+ 18 - 0
render/post_processing/example/materials/invert.vp

@@ -0,0 +1,18 @@
+#version 140
+
+in highp vec4 position;
+in mediump vec2 texcoord0;
+
+out mediump vec2 var_texcoord0;
+
+uniform vs_uniforms
+{
+    highp mat4 view_proj;
+};
+
+void main()
+{
+    gl_Position = view_proj * vec4(position.xyz, 1.0);
+    var_texcoord0 = texcoord0;
+}
+

+ 14 - 0
render/post_processing/example/materials/passthrough.fp

@@ -0,0 +1,14 @@
+#version 140
+
+in mediump vec2 var_texcoord0;
+
+out vec4 out_fragColor;
+
+uniform mediump sampler2D tex0;
+
+void main()
+{
+    vec4 color = texture(tex0, var_texcoord0.xy);
+    out_fragColor = vec4(color.rgb,1.0);
+}
+

+ 8 - 0
render/post_processing/example/materials/passthrough.material

@@ -0,0 +1,8 @@
+name: "passthrough"
+tags: "postprocess"
+vertex_program: "/example/materials/passthrough.vp"
+fragment_program: "/example/materials/passthrough.fp"
+vertex_constants {
+  name: "view_proj"
+  type: CONSTANT_TYPE_VIEWPROJ
+}

+ 18 - 0
render/post_processing/example/materials/passthrough.vp

@@ -0,0 +1,18 @@
+#version 140
+
+in highp vec4 position;
+in mediump vec2 texcoord0;
+
+out mediump vec2 var_texcoord0;
+
+uniform vs_uniforms
+{
+    highp mat4 view_proj;
+};
+
+void main()
+{
+    gl_Position = view_proj * vec4(position.xyz, 1.0);
+    var_texcoord0 = texcoord0;
+}
+

+ 24 - 0
render/post_processing/example/materials/scanlines.fp

@@ -0,0 +1,24 @@
+#version 140
+
+in mediump vec2 var_texcoord0;
+
+out vec4 out_fragColor;
+
+uniform mediump sampler2D tex0;
+uniform fs_uniforms
+{
+	mediump vec4 resolution;
+};
+
+// https://www.shadertoy.com/view/XdXXD4
+void main()
+{
+	vec2 uv = var_texcoord0.xy;
+	vec4 col = texture(tex0, uv );
+
+	// scanline
+	float scanline = sin(uv.y*resolution.y)*0.04;
+	col -= scanline;
+
+	out_fragColor = col;
+}

+ 14 - 0
render/post_processing/example/materials/scanlines.material

@@ -0,0 +1,14 @@
+name: "scanlines"
+tags: "postprocess"
+vertex_program: "/example/materials/scanlines.vp"
+fragment_program: "/example/materials/scanlines.fp"
+vertex_constants {
+  name: "view_proj"
+  type: CONSTANT_TYPE_VIEWPROJ
+}
+fragment_constants {
+  name: "resolution"
+  type: CONSTANT_TYPE_USER
+  value {
+  }
+}

+ 17 - 0
render/post_processing/example/materials/scanlines.vp

@@ -0,0 +1,17 @@
+#version 140
+
+in highp vec4 position;
+in mediump vec2 texcoord0;
+
+out mediump vec2 var_texcoord0;
+
+uniform vs_uniforms
+{
+    highp mat4 view_proj;
+};
+
+void main()
+{
+    gl_Position = view_proj * vec4(position.xyz, 1.0);
+    var_texcoord0 = texcoord0;
+}

+ 49 - 0
render/post_processing/example/orbit_camera.script

@@ -0,0 +1,49 @@
+-- The initial zoom level
+go.property("zoom", 3)
+-- The speed of the zoom
+go.property("zoom_speed", 0.1)
+-- The speed of the rotation
+go.property("rotation_speed", 0.5)
+-- The offset of the camera from the origin
+go.property("offset", vmath.vector3(0, 0, 0))
+
+function init(self)
+	-- Acquire input focus to receive input events
+	msg.post(".", "acquire_input_focus")
+
+	-- Initialize start values
+	self.yaw = go.get(".", "euler.y")
+	self.pitch = go.get(".", "euler.x")
+	self.zoom_offset = 0
+	self.current_yaw = self.yaw
+	self.current_pitch = self.pitch
+	self.current_zoom = self.zoom_offset
+end
+
+function update(self, dt)
+	-- Animate camera rotation and zoom
+	self.current_yaw = vmath.lerp(0.15, self.current_yaw, self.yaw)
+	self.current_pitch = vmath.lerp(0.15, self.current_pitch, self.pitch)
+	self.current_zoom = vmath.lerp(0.15, self.current_zoom, self.zoom_offset)
+
+	-- Calculate rotation and position
+	local camera_yaw = vmath.quat_rotation_y(math.rad(self.current_yaw))
+	local camera_pitch = vmath.quat_rotation_x(math.rad(self.current_pitch))
+	local camera_rotation = camera_yaw * camera_pitch
+	local camera_position = self.offset + vmath.rotate(camera_rotation, vmath.vector3(0, 0, self.zoom + self.current_zoom))
+
+	-- Set camera position and rotation
+	go.set_position(camera_position)
+	go.set_rotation(camera_rotation)
+end
+
+function on_input(self, action_id, action)
+	if action_id == hash("touch") and not action.pressed then
+		self.yaw   = self.yaw   - action.dx * self.rotation_speed
+		self.pitch = self.pitch + action.dy * self.rotation_speed
+	elseif action_id == hash("mouse_wheel_up") then
+		self.zoom_offset = self.zoom_offset - self.zoom * self.zoom_speed
+	elseif action_id == hash("mouse_wheel_down") then
+		self.zoom_offset = self.zoom_offset + self.zoom * self.zoom_speed
+	end
+end

+ 20 - 0
render/post_processing/example/player.script

@@ -0,0 +1,20 @@
+function init(self)
+	msg.post(".", "acquire_input_focus")
+	model.play_anim("#model", "Idle", go.PLAYBACK_LOOP_FORWARD)
+
+	-- enabled and disable meshes to get the correct look
+	-- weapons
+	model.set_mesh_enabled("#model", "1H_Sword", true)
+	model.set_mesh_enabled("#model", "1H_Sword_Offhand", false)
+	model.set_mesh_enabled("#model", "2H_Sword", false)
+
+	-- equipment
+	model.set_mesh_enabled("#model", "Knight_Helmet", true)
+	model.set_mesh_enabled("#model", "Knight_Cape", true)
+
+	-- different shields
+	model.set_mesh_enabled("#model", "Spike_Shield", true)
+	model.set_mesh_enabled("#model", "Round_Shield", false)
+	model.set_mesh_enabled("#model", "Rectangle_Shield", false)
+	model.set_mesh_enabled("#model", "Badge_Shield", false)
+end

+ 1 - 0
render/post_processing/example/postprocess.render

@@ -0,0 +1 @@
+script: "/example/postprocess.render_script"

+ 331 - 0
render/post_processing/example/postprocess.render_script

@@ -0,0 +1,331 @@
+-- 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
+
+local function create_postprocess_rt(self, width, height)
+    local color_params = {
+        format = graphics.TEXTURE_FORMAT_RGBA,
+        width = width,
+        height = height,
+        min_filter = render.FILTER_LINEAR,
+        mag_filter = render.FILTER_LINEAR,
+        u_wrap = render.WRAP_CLAMP_TO_EDGE,
+        v_wrap = render.WRAP_CLAMP_TO_EDGE
+    }
+    local depth_params = {
+        format = graphics.TEXTURE_FORMAT_DEPTH,
+        width = width,
+        height = height,
+    }
+    self.postprocess_rt =  render.render_target("postprocess_rt", { [render.BUFFER_COLOR_BIT] = color_params, [render.BUFFER_DEPTH_BIT] = depth_params } )
+    self.postprocess_rt_width = width
+    self.postprocess_rt_height = height
+end
+
+local function update_postprocess_rt(self)
+    local w = render.get_window_width()
+    local h = render.get_window_height()
+    
+    -- keep render target if size is the same
+    if self.postprocess_rt_width == w and self.postprocess_rt_height == h then
+        return
+    end
+
+    render.delete_render_target(self.postprocess_rt)
+    create_postprocess_rt(self, w, h)
+end
+
+function init(self)
+    -- create the postprocess predicate and all of the default predicates
+    self.predicates = create_predicates("postprocess", "tile", "gui", "particle", "model", "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 })
+
+    create_postprocess_rt(self, render.get_window_width(), render.get_window_height())
+
+    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)
+    update_postprocess_rt(self)
+
+    local state = self.state
+    if not state.valid then
+        if not update_state(state) then
+            return
+        end
+    end
+
+    -- enable postprecssing render target
+    -- subsequent draw operations will be done to the render target
+    --
+    render.set_render_target(self.postprocess_rt)
+    
+    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 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)
+
+
+    -- revert to the default render target
+    --
+    render.set_render_target(render.RENDER_TARGET_DEFAULT)
+    
+    -- render post processing render target to quad with predicate 'postprocess'
+    --
+    render.set_view(vmath.matrix4())
+    render.set_projection(vmath.matrix4())
+    render.enable_texture(0, self.postprocess_rt, render.BUFFER_COLOR_BIT)
+    render.draw(predicates.postprocess)
+    render.disable_texture(0)
+    
+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

+ 20 - 0
render/post_processing/example/postprocess.script

@@ -0,0 +1,20 @@
+go.property("invert", resource.material("/example/materials/invert.material"))
+go.property("passthrough", resource.material("/example/materials/passthrough.material"))
+go.property("scanlines", resource.material("/example/materials/scanlines.material"))
+
+function init(self)
+	msg.post(".", "acquire_input_focus")
+	go.set("#quad", "material", self.invert)
+end
+
+function on_input(self, action_id, action)
+	if action_id == hash("key_1") then
+		go.set("#quad", "material", self.invert)
+	elseif action_id == hash("key_2") then
+		go.set("#quad", "material", self.scanlines)
+		local w, h = window.get_size()
+		go.set("#quad", "resolution", vmath.vector4(w, h, 0, 0))
+	elseif action_id == hash("key_3") then
+		go.set("#quad", "material", self.passthrough)
+	end
+end

+ 66 - 0
render/post_processing/game.project

@@ -0,0 +1,66 @@
+[project]
+title = Post-process
+version = 0.1
+
+[bootstrap]
+main_collection = /example/example.collectionc
+render = /example/postprocess.renderc
+
+[input]
+game_binding = /builtins/input/all.input_bindingc
+repeat_interval = 0.05
+
+[display]
+width = 720
+height = 720
+high_dpi = 1
+
+[physics]
+scale = 0.02
+gravity_y = -500.0
+
+[script]
+shared_state = 1
+
+[collection_proxy]
+max_count = 256
+
+[label]
+subpixels = 1
+
+[sprite]
+subpixels = 1
+max_count = 32765
+
+[windows]
+iap_provider = 
+
+[android]
+package = com.defold.examples
+
+[ios]
+bundle_identifier = com.defold.examples
+
+[osx]
+bundle_identifier = com.defold.examples
+
+[html5]
+show_fullscreen_button = 0
+show_made_with_defold = 0
+scale_mode = no_scale
+heap_size = 64
+
+[graphics]
+texture_profiles = /all.texture_profiles
+
+[collection]
+max_instances = 32765
+
+[particle_fx]
+max_emitter_count = 1024
+
+[render]
+clear_color_blue = 1.0
+clear_color_green = 1.0
+clear_color_red = 1.0
+

BIN
render/post_processing/invert_material.png


BIN
render/post_processing/material_properties.png


BIN
render/post_processing/postprocess_thumb.png


BIN
render/post_processing/quad.png