diff --git a/render/basic_lights_2d/all.texture_profiles b/render/basic_lights_2d/all.texture_profiles new file mode 100644 index 00000000..5b0d7762 --- /dev/null +++ b/render/basic_lights_2d/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 + } +} diff --git a/render/basic_lights_2d/assets/images/elementStone019.png b/render/basic_lights_2d/assets/images/elementStone019.png new file mode 100755 index 00000000..0a85d3f2 Binary files /dev/null and b/render/basic_lights_2d/assets/images/elementStone019.png differ diff --git a/render/basic_lights_2d/assets/images/green.png b/render/basic_lights_2d/assets/images/green.png new file mode 100644 index 00000000..81197caa Binary files /dev/null and b/render/basic_lights_2d/assets/images/green.png differ diff --git a/render/basic_lights_2d/assets/sprites.atlas b/render/basic_lights_2d/assets/sprites.atlas new file mode 100644 index 00000000..9b9109f0 --- /dev/null +++ b/render/basic_lights_2d/assets/sprites.atlas @@ -0,0 +1,7 @@ +images { + image: "/assets/images/green.png" +} +images { + image: "/assets/images/elementStone019.png" +} +extrude_borders: 2 diff --git a/render/basic_lights_2d/example.md b/render/basic_lights_2d/example.md new file mode 100644 index 00000000..a80b2756 --- /dev/null +++ b/render/basic_lights_2d/example.md @@ -0,0 +1,153 @@ +--- +tags: render, material, sprite +title: Basic Lights 2D +brief: This example shows how to make sprites respond to Defold Light components with a custom material in a basic setup. +author: Defold Foundation +scripts: lit_sprite.vp, lit_sprite.fp, follow_cursor.script +thumbnail: thumbnail.webp +--- + +The example shows how to utilise Light components (introduced in Defold 1.13.1) +and materials to add a simple lighting to the sprites. +The light illuminates every sprite that uses the lit material. +This is a forward-lighting technique: each sprite calculates its lighting as it is drawn. + +Read more about [Light components in the manual](https://defold.com/manuals/light/). + +## What You'll Learn + +- How a sprite shader can read and react to Light components data. +- How to use a fixed surface normal for simple 2D lighting. +- When this single-pass technique is preferable to a screen-space light map. + +## Setup + +The collection contains: +- `ambient_light` game object with ambient light component +- `point_light` game object with point light component and a `follow_cursor` script +- `spot_light` game object with spot light component +- `scene` consisting of game objects with sprites for background and three stones. + +![setup](setup.png) + +Sprite components of the scene elements all use `lit_sprite.material`, +which makes them respond to Light components. + +![sprites](sprites.png) + +The scene uses 3 lights: + +- An Ambient Light with a blue-grey color and an intensity of `0.5`. +It provides the minimum illumination across the whole scene. +- A white Point Light with an intensity of `3.0` and a range of `360`. +Its game object starts at `Z = 140`, placing the light above the sprites on the positive Z axis. +The Point Light game object also contains `light.script`. +The script moves the light to the mouse cursor, +demonstrating that Light components follow their game object's world transform automatically. +- A red Spot Light with an intensity of `3.0` and a range of `800`, +outter cone angle - `50` and inner cone angle - `30`. + +## How It Works + +### Light component data + +The engine collects light components data and exposes it to materials +whose shaders declare the built-in `LightBuffer` layout. + +Ambient lights are accumulated separately as `color × intensity`. + +Directional, Point, and Spot lights are stored in the light array +with their world position or direction, color, intensity, range, and type-specific values. +Read more about light components in the Lights manual. + +A Point Light uses the position of its game object. +Its effective range is the component's Range multiplied +by the smallest absolute axis of the game object's world scale. +Moving or uniformly scaling the game object therefore moves +or resizes the illuminated area automatically +without a need to change shader constants from Lua. + +The light component itself does not select which sprites it affects. +Any material that consumes the global light buffer can respond to it, +while sprites using the regular built-in sprite material remain unchanged. + +![material](material.png) + +### Vertex shader + +Sprite vertices are in world space. `lit_sprite.vp` uses the material's `view_proj` +constant to project each vertex to the screen, just like a regular sprite shader. +It also uses `mtx_view` to pass the fragment position in view space to the fragment shader. + +The built-in lighting functions operate in view space, so the surface position, +surface normal, and light position must all use the same coordinate space. +Passing `mtx_view` also keeps the material correct if the scene would later use a different camera. + +### Fragment shader + +`lit_sprite.fp` defines `MAX_LIGHT_COUNT` and includes `/builtins/materials/lighting.glsl`. +The include declares the engine-owned light buffer and provides two helpers used here: + +- `ambient_light()` returns the accumulated Ambient Light contribution. +- `diffuse_lambert(normal, position)` evaluates the active Directional, Point, and Spot lights up to `MAX_LIGHT_COUNT`. + +Sprites have positions and texture coordinates, but no normal vertex attribute. +This example treats every sprite as a flat surface facing positive Z +and transforms that fixed `(0, 0, 1)` direction into view space. +A Point Light must therefore have a positive Z offset: +if it were placed directly in the same plane as sprite, the light direction would be perpendicular +to the fixed normal and its Lambert contribution would be zero. + +For a Point Light, the included shader calculates the contribution from three factors: + +1. Light color multiplied by intensity. +2. Lambert diffuse shading: `max(dot(normal, direction_to_light), 0)`. +3. Distance attenuation: `clamp(1 - distance / range, 0, 1)²`. + +The fragment shader adds the ambient and diffuse contributions, +multiplies the sampled premultiplied sprite color by the result, +and preserves the texture alpha. + +### Light count + +This shader sets `MAX_LIGHT_COUNT` to `4`. +The value is the maximum number of non-ambient lights this material evaluates. +It must not exceed `[light] max_count` in `game.project`. +This example leaves that project setting at its default of `64`. +If more than four non-ambient lights are active, this shader evaluates only the first four. +Ambient lights do not occupy entries in the shader array because the engine combines them into one ambient value, +although every Ambient Light still counts as a Light component in the project limit. + +### Input and rendering + +`light.script` acquires input focus and handles mouse movement in `on_input()`. +It copies the cursor's X and Y coordinates to the Point Light game object +while preserving the positive Z offset required by the flat surface normal. +Light transforms are read by the engine after game object updates, +so the shader receives the new world position each frame. + +No custom render script or render target is needed. +The default renderer draws the sprites normally, +and the engine binds the light buffer automatically before drawing a material that declares it. + +## When to Use This Technique + +This approach is useful for 2D games that need colored light and smooth distance falloff. +It keeps the setup close to regulat sprite rendering +and allows the same Light components to be shared with lit 3D materials. + +- Every sprite uses the same flat normal +- There are no shadows, occluders, or visibility tests - light passes across all lit sprites within range. +- The shader implements diffuse (Lambert) and ambient lighting only. It has no specular, emissive, etc. +- Every lit fragment evaluates the active lights up to `MAX_LIGHT_COUNT`, +so large scenes should choose the limit deliberately or use a more selective lighting design. + +For normal-mapped 2D art, replace the fixed normal with a normal sampled from a texture +and transform it into the same coordinate space as the light calculations. + +For shadows or large numbers of purely 2D lights, other techniques like e.g. +a screen-space light-map pipeline may be more appropriate. + +See the Defold manuals for more detail about [materials](https://defold.com/manuals/material/), [shaders](https://defold.com/manuals/shader/), and the [render pipeline](https://defold.com/manuals/render/). + +The stone image is CC0 artwork from Kenney's Platformer Art Extended pack. diff --git a/render/basic_lights_2d/example/follow_cursor.script b/render/basic_lights_2d/example/follow_cursor.script new file mode 100644 index 00000000..6173ff2a --- /dev/null +++ b/render/basic_lights_2d/example/follow_cursor.script @@ -0,0 +1,17 @@ +function init(self) + msg.post(".", "acquire_input_focus") -- <1> +end + +function on_input(self, action_id, action) + if action_id == nil then + local position = go.get_position() + position.x = action.x + position.y = action.y + go.set_position(position) -- <2> + end +end + +--[[ +1. Acquire input focus so this script receives mouse movement. +2. Move the Point Light game object to the cursor while preserving its Z position. +]] diff --git a/render/basic_lights_2d/example/lights_2d.collection b/render/basic_lights_2d/example/lights_2d.collection new file mode 100644 index 00000000..29d20022 --- /dev/null +++ b/render/basic_lights_2d/example/lights_2d.collection @@ -0,0 +1,262 @@ +name: "lights_2d" +scale_along_z: 0 +embedded_instances { + id: "ambient_light" + data: "embedded_components {\n" + " id: \"ambient_light\"\n" + " type: \"ambient_light\"\n" + " data: \"data {\\n" + " struct {\\n" + " fields {\\n" + " key: \\\"color\\\"\\n" + " value {\\n" + " list {\\n" + " values {\\n" + " number: 0.16000000000000003\\n" + " }\\n" + " values {\\n" + " number: 0.18000000000000002\\n" + " }\\n" + " values {\\n" + " number: 0.22000000000000003\\n" + " }\\n" + " }\\n" + " }\\n" + " }\\n" + " fields {\\n" + " key: \\\"intensity\\\"\\n" + " value {\\n" + " number: 0.5\\n" + " }\\n" + " }\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "" +} +embedded_instances { + id: "point_light" + data: "components {\n" + " id: \"script\"\n" + " component: \"/example/follow_cursor.script\"\n" + "}\n" + "embedded_components {\n" + " id: \"point_light\"\n" + " type: \"point_light\"\n" + " data: \"data {\\n" + " struct {\\n" + " fields {\\n" + " key: \\\"color\\\"\\n" + " value {\\n" + " list {\\n" + " values {\\n" + " number: 1.0\\n" + " }\\n" + " values {\\n" + " number: 1.0\\n" + " }\\n" + " values {\\n" + " number: 1.0\\n" + " }\\n" + " }\\n" + " }\\n" + " }\\n" + " fields {\\n" + " key: \\\"intensity\\\"\\n" + " value {\\n" + " number: 3.0\\n" + " }\\n" + " }\\n" + " fields {\\n" + " key: \\\"range\\\"\\n" + " value {\\n" + " number: 360.0\\n" + " }\\n" + " }\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + x: 160.0 + y: 390.0 + z: 140.0 + } +} +embedded_instances { + id: "spot_light" + data: "embedded_components {\n" + " id: \"spot_light\"\n" + " type: \"spot_light\"\n" + " data: \"data {\\n" + " struct {\\n" + " fields {\\n" + " key: \\\"color\\\"\\n" + " value {\\n" + " list {\\n" + " values {\\n" + " number: 1.0\\n" + " }\\n" + " values {\\n" + " number: 0.0\\n" + " }\\n" + " values {\\n" + " number: 0.0\\n" + " }\\n" + " }\\n" + " }\\n" + " }\\n" + " fields {\\n" + " key: \\\"inner_cone_angle\\\"\\n" + " value {\\n" + " number: 30.0\\n" + " }\\n" + " }\\n" + " fields {\\n" + " key: \\\"intensity\\\"\\n" + " value {\\n" + " number: 3.0\\n" + " }\\n" + " }\\n" + " fields {\\n" + " key: \\\"outer_cone_angle\\\"\\n" + " value {\\n" + " number: 50.0\\n" + " }\\n" + " }\\n" + " fields {\\n" + " key: \\\"range\\\"\\n" + " value {\\n" + " number: 800.0\\n" + " }\\n" + " }\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + x: 348.0 + y: 14.0 + z: 200.0 + } + rotation { + x: 0.38268343 + w: 0.9238795 + } +} +embedded_instances { + id: "scene" + children: "background" + children: "stone" + children: "stone1" + children: "stone2" + data: "" +} +embedded_instances { + id: "background" + data: "embedded_components {\n" + " id: \"sprite\"\n" + " type: \"sprite\"\n" + " data: \"default_animation: \\\"green\\\"\\n" + "material: \\\"/example/lit_sprite.material\\\"\\n" + "size {\\n" + " x: 720.0\\n" + " y: 720.0\\n" + "}\\n" + "size_mode: SIZE_MODE_MANUAL\\n" + "textures {\\n" + " sampler: \\\"texture_sampler\\\"\\n" + " texture: \\\"/assets/sprites.atlas\\\"\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + x: 360.0 + y: 360.0 + z: -0.2 + } +} +embedded_instances { + id: "stone" + data: "embedded_components {\n" + " id: \"sprite\"\n" + " type: \"sprite\"\n" + " data: \"default_animation: \\\"elementStone019\\\"\\n" + "material: \\\"/example/lit_sprite.material\\\"\\n" + "textures {\\n" + " sampler: \\\"texture_sampler\\\"\\n" + " texture: \\\"/assets/sprites.atlas\\\"\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + x: 356.0 + y: 494.0 + } +} +embedded_instances { + id: "stone1" + data: "embedded_components {\n" + " id: \"sprite\"\n" + " type: \"sprite\"\n" + " data: \"default_animation: \\\"elementStone019\\\"\\n" + "material: \\\"/example/lit_sprite.material\\\"\\n" + "size {\\n" + " x: 220.0\\n" + " y: 140.0\\n" + "}\\n" + "textures {\\n" + " sampler: \\\"texture_sampler\\\"\\n" + " texture: \\\"/assets/sprites.atlas\\\"\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + x: 149.0 + y: 278.0 + } + rotation { + z: -0.25881904 + w: 0.9659258 + } + scale3 { + y: 0.8 + } +} +embedded_instances { + id: "stone2" + data: "embedded_components {\n" + " id: \"sprite\"\n" + " type: \"sprite\"\n" + " data: \"default_animation: \\\"elementStone019\\\"\\n" + "material: \\\"/example/lit_sprite.material\\\"\\n" + "size {\\n" + " x: 220.0\\n" + " y: 140.0\\n" + "}\\n" + "textures {\\n" + " sampler: \\\"texture_sampler\\\"\\n" + " texture: \\\"/assets/sprites.atlas\\\"\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + x: 497.0 + y: 279.0 + } + rotation { + z: 0.17364818 + w: 0.9848077 + } + scale3 { + x: 0.8 + y: 0.8 + } +} diff --git a/render/basic_lights_2d/example/lit_sprite.fp b/render/basic_lights_2d/example/lit_sprite.fp new file mode 100644 index 00000000..c2494975 --- /dev/null +++ b/render/basic_lights_2d/example/lit_sprite.fp @@ -0,0 +1,20 @@ +#version 140 + +in highp vec4 var_position; +in mediump vec2 var_texcoord0; +in highp mat4 var_view; + +out vec4 out_fragColor; + +uniform mediump sampler2D texture_sampler; + +#define MAX_LIGHT_COUNT 4 +#include "/builtins/materials/lighting.glsl" + +void main() +{ + vec4 color = texture(texture_sampler, var_texcoord0); + vec3 normal = normalize((var_view * vec4(0.0, 0.0, 1.0, 0.0)).xyz); + vec3 light = ambient_light() + diffuse_lambert(normal, var_position.xyz); + out_fragColor = vec4(color.rgb * light, color.a); +} diff --git a/render/basic_lights_2d/example/lit_sprite.material b/render/basic_lights_2d/example/lit_sprite.material new file mode 100644 index 00000000..66c5654a --- /dev/null +++ b/render/basic_lights_2d/example/lit_sprite.material @@ -0,0 +1,19 @@ +name: "sprite_lit_2d" +tags: "tile" +vertex_program: "/example/lit_sprite.vp" +fragment_program: "/example/lit_sprite.fp" +vertex_constants { + name: "view_proj" + type: CONSTANT_TYPE_VIEWPROJ +} +vertex_constants { + name: "mtx_view" + type: CONSTANT_TYPE_VIEW +} +samplers { + name: "texture_sampler" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_DEFAULT + filter_mag: FILTER_MODE_MAG_DEFAULT +} diff --git a/render/basic_lights_2d/example/lit_sprite.vp b/render/basic_lights_2d/example/lit_sprite.vp new file mode 100644 index 00000000..0fdfb439 --- /dev/null +++ b/render/basic_lights_2d/example/lit_sprite.vp @@ -0,0 +1,22 @@ +#version 140 + +in highp vec4 position; +in mediump vec2 texcoord0; + +out highp vec4 var_position; +out mediump vec2 var_texcoord0; +out highp mat4 var_view; + +uniform vs_uniforms +{ + highp mat4 view_proj; + highp mat4 mtx_view; +}; + +void main() +{ + var_position = mtx_view * vec4(position.xyz, 1.0); + var_texcoord0 = texcoord0; + var_view = mtx_view; + gl_Position = view_proj * vec4(position.xyz, 1.0); +} diff --git a/render/basic_lights_2d/game.project b/render/basic_lights_2d/game.project new file mode 100644 index 00000000..321e3814 --- /dev/null +++ b/render/basic_lights_2d/game.project @@ -0,0 +1,64 @@ +[project] +title = Basic Lights 2D + +[bootstrap] +main_collection = /example/lights_2d.collectionc + +[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_red = 0.160156 +clear_color_green = 0.164063 +clear_color_blue = 0.183594 + diff --git a/render/basic_lights_2d/material.png b/render/basic_lights_2d/material.png new file mode 100644 index 00000000..0d1ce2d0 Binary files /dev/null and b/render/basic_lights_2d/material.png differ diff --git a/render/basic_lights_2d/setup.png b/render/basic_lights_2d/setup.png new file mode 100644 index 00000000..f2d90a00 Binary files /dev/null and b/render/basic_lights_2d/setup.png differ diff --git a/render/basic_lights_2d/sprites.png b/render/basic_lights_2d/sprites.png new file mode 100644 index 00000000..77de7c19 Binary files /dev/null and b/render/basic_lights_2d/sprites.png differ diff --git a/render/basic_lights_2d/thumbnail.webp b/render/basic_lights_2d/thumbnail.webp new file mode 100644 index 00000000..00fdaacf Binary files /dev/null and b/render/basic_lights_2d/thumbnail.webp differ