Merge: gamnit: premultiply alpha for a nicer alpha blending
authorJean Privat <jean@pryen.org>
Wed, 28 Jun 2017 19:39:24 +0000 (15:39 -0400)
committerJean Privat <jean@pryen.org>
Wed, 28 Jun 2017 19:39:24 +0000 (15:39 -0400)
Premultiplying RBG pixels values by their alpha reduces visible artifacts when blending between different opacity levels. It also allows for mixing obstructing blending (like smoke hiding the background) and additive blending (like explosions amplifying each other, as in the second screenshot below).

Here are the old and new explosion effects in Jump 'n' Gun (using the same texture as Action Nitro):
![old](https://user-images.githubusercontent.com/208057/27644667-22a0f3d0-5bf2-11e7-884b-2e21555e5e68.png) ![new](https://user-images.githubusercontent.com/208057/27644776-619601c0-5bf2-11e7-84c4-f884533efaf0.png)

By default, textures are expected to be straight (the classic not-premultiplied) and are premultiplied at loading. It can be deactivated for asset files that are already premultiplied (as with the explosion texture above).

The color services in both APIs are also affected. `Sprite::tint` remains the same, using straight colors, the easier alternative. However, `SmoothMaterial::ambient_color`, `diffuse_color` and `specular_color` should be premultiplied, giving more possibility to the clients. Alternatively, `Actor::alpha` is applied to material colors, so while RGB values of `ambient_color` are not premultiplied by `ambient_color[3]` they are premultiplied by `Actor::alpha`.

On Android, pixel values are automatically premultiplied when loaded from a file by the system, so we unpremultiply them as needed (with a strong possibility of data loss on additive blending effects). To fix this, this PR intro a new module `gamnit::android19` using services from the API 19 to disable the premultiply. This module is optional (for now) but it should be imported by all Android games with premultiplied texture asset files.

As a bonus, fix the support for Action Nitro on Android via F-Droid. The latest performance improvements to gamnit makes the game run nicely on mobiles devices!

Pull-Request: #2518

14 files changed:
contrib/action_nitro/Makefile
contrib/action_nitro/art/icon.svg
contrib/action_nitro/art/icon_background.png [new file with mode: 0644]
contrib/action_nitro/assets/particles/blood07.png
contrib/action_nitro/assets/particles/explosion00.png
contrib/action_nitro/package.ini
contrib/action_nitro/src/action_nitro.nit
contrib/model_viewer/src/globe.nit
lib/gamnit/android19.nit [new file with mode: 0644]
lib/gamnit/depth/more_materials.nit
lib/gamnit/depth/particles.nit
lib/gamnit/display_android.nit
lib/gamnit/flat.nit
lib/gamnit/textures.nit

index b3fcebc..81db63b 100644 (file)
@@ -1,14 +1,14 @@
-NITC=nitc
-NITLS=nitls
-
 all: bin/action_nitro
 
-bin/action_nitro: $(shell ${NITLS} -M src/action_nitro.nit -m linux) pre-build
-       ${NITC} src/action_nitro.nit -m linux -o $@
+bin/action_nitro: $(shell nitls -M src/action_nitro.nit -m linux) pre-build
+       nitc src/action_nitro.nit -m linux -o $@
+
+android: bin/action_nitro.apk
+bin/action_nitro.apk: $(shell nitls -M src/action_nitro.nit -m gamnit::android19 -m src/touch_ui.nit) pre-build android/res/
+       nitc src/action_nitro.nit -m gamnit::android19 -m src/touch_ui.nit -o $@
 
-android:
-bin/action_nitro.apk: $(shell ${NITLS} -M src/action_nitro.nit -m android -m src/touch_ui.nit) pre-build android/res/
-       ${NITC} src/action_nitro.nit -m android -m src/touch_ui.nit -o $@
+android-release: $(shell nitls -M src/action_nitro.nit -m gamnit::android19 -m src/touch_ui.nit) pre-build android/res/
+       nitc src/action_nitro.nit -m gamnit::android19 -m src/touch_ui.nit -o bin/action_nitro.apk --release
 
 src/gen/texts.nit: art/texts.svg
        make -C ../inkscape_tools/
index 7383208..a0701e1 100644 (file)
@@ -60,8 +60,8 @@
      id="layer1"
      transform="translate(-286.05811,81.477047)">
     <image
-       sodipodi:absref="/home/xymus/projects/nit/contrib/action_nitro/art/../assets/particles/explosion00.png"
-       xlink:href="../assets/particles/explosion00.png"
+       sodipodi:absref="icon_background.png"
+       xlink:href="icon_background.png"
        width="479.1073"
        height="439.72864"
        id="image3062"
diff --git a/contrib/action_nitro/art/icon_background.png b/contrib/action_nitro/art/icon_background.png
new file mode 100644 (file)
index 0000000..22529ca
Binary files /dev/null and b/contrib/action_nitro/art/icon_background.png differ
index 584e6d1..4c590d5 100644 (file)
Binary files a/contrib/action_nitro/assets/particles/blood07.png and b/contrib/action_nitro/assets/particles/blood07.png differ
index 22529ca..16a484f 100644 (file)
Binary files a/contrib/action_nitro/assets/particles/explosion00.png and b/contrib/action_nitro/assets/particles/explosion00.png differ
index b9ae919..bcd9ef0 100644 (file)
@@ -9,3 +9,4 @@ git=https://github.com/nitlang/nit.git
 git.directory=contrib/action_nitro/
 homepage=http://nitlanguage.org
 issues=https://github.com/nitlang/nit/issues
+apk=http://nitlanguage.org/fdroid/apk/action_nitro.apk
index 4fdf39e..2c6afdc 100644 (file)
@@ -18,7 +18,6 @@ module action_nitro is
        app_version(1, 0, git_revision)
 
        android_manifest_activity """android:screenOrientation="sensorLandscape""""
-       android_api_target 10
 end
 
 import gamnit::depth
@@ -80,11 +79,11 @@ redef class App
        # Particle effects
 
        # Explosion particles
-       var explosions = new ParticleSystem(20, explosion_program,
+       var explosions = new ParticleSystem(100, explosion_program,
                new Texture("particles/explosion00.png"))
 
        # Blood explosion particles
-       var blood = new ParticleSystem(20, explosion_program,
+       var blood = new ParticleSystem(100, explosion_program,
                new Texture("particles/blood07.png"))
 
        # Smoke for the background
@@ -137,6 +136,9 @@ redef class App
 
        redef fun on_create
        do
+               blood.texture.as(RootTexture).premultiply_alpha = false
+               explosions.texture.as(RootTexture).premultiply_alpha = false
+
                super
 
                show_splash_screen new Texture("textures/splash.jpg")
@@ -150,10 +152,10 @@ redef class App
                ui_camera.reset_height 1080.0
 
                # Register particle systems
-               particle_systems.add explosions
-               particle_systems.add blood
                particle_systems.add smoke
                particle_systems.add clouds
+               particle_systems.add blood
+               particle_systems.add explosions
 
                # Stars background
                sprites.add stars
@@ -468,12 +470,12 @@ redef class Human
        # Show death animation (explosion)
        fun death_animation
        do
-               var force = 4.0
+               var force = 2.0
                health = 0.0
-               for i in 32.times do
+               for i in 16.times do
                        app.blood.add(
                                new Point3d[Float](center.x & force, center.y & force, center.z & force),
-                               (2048.0 & 4096.0) * force, 0.3 & 0.1)
+                               (4096.0 & 2048.0) * force, 0.3 & 0.1)
                end
        end
 end
@@ -622,26 +624,16 @@ redef class World
                super
 
                # Particles
-               app.explosions.add(center, 8192.0 * force, 0.3)
-               for i in (4.0*force).to_i.times do
+               var range = 0.5 * force
+               app.explosions.add(center, 4096.0 * force, 0.3)
+               for i in (2.0*force).to_i.times do
                        app.explosions.add(
-                               new Point3d[Float](center.x & force, center.y & force/2.0, center.z & force),
-                               (4096.0 & 2048.0) * force, 0.3 & 0.3, 0.5.rand)
+                               new Point3d[Float](center.x & range, center.y & range, center.z & range),
+                               (2048.0 & 1024.0) * force, 0.3 & 0.3, 0.5.rand)
                end
        end
 end
 
-redef class Int
-       # Pad a number with `0`s on the left side to reach `size` digits
-       private fun pad(size: Int): String
-       do
-               var s = to_s
-               var d = size - s.length
-               if d > 0 then s = "0"*d + s
-               return s
-       end
-end
-
 # Manager to display numbers in sprite
 class CounterSprites
 
@@ -694,8 +686,8 @@ redef class SmokeProgram
                gl_PointSize = scale / gl_Position.z * (pt+0.1);
 
                if (pt < 0.1)
-                       v_color.a = pt / 0.1;
+                       v_color *= pt / 0.1;
                else
-                       v_color.a = 1.0 - pt*0.9;
+                       v_color *= 1.0 - pt*0.9;
        """
 end
index a5a9a62..12c6ffc 100644 (file)
@@ -91,10 +91,12 @@ class GlobeMaterial
        init surface do init(0, true, [1.0, 1.0, 1.0, 1.0])
 
        # Create and configure a material for the cloud layer
-       init clouds do init(4, false, [1.0, 1.0, 1.0, 0.5])
+       init clouds do init(4, false, [1.0*clouds_a, 1.0*clouds_a, 1.0*clouds_a, clouds_a])
+       private var clouds_a = 0.5
 
        # Create and configure a material for the visible atmosphere
-       init atmo do init(null, false, [0.0, 0.8, 1.0, 0.05])
+       init atmo do init(null, false, [0.0, 0.8*atmo_a, 1.0*atmo_a, atmo_a])
+       private var atmo_a = 0.05
 
        redef fun draw(actor, model)
        do
@@ -228,7 +230,6 @@ class GlobeProgram
                                s += 0.05 * texture2D(tex_displace, tex_coord).r;
 
                        gl_Position = (vec4(coord.xyz * s, 1.0) * rotation + translation) * mvp;
-
                }
                """ @ glsl_vertex_shader
 
diff --git a/lib/gamnit/android19.nit b/lib/gamnit/android19.nit
new file mode 100644 (file)
index 0000000..3325ece
--- /dev/null
@@ -0,0 +1,55 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+
+# Variation using features from Android API 19
+#
+# Add support for `TextureAsset::premultiply_alpha = false` on Android.
+module android19 is
+       android_api_min 19
+       android_api_target 22
+end
+
+import android
+intrude import display_android
+intrude import android::load_image
+
+in "Java" `{
+       import android.graphics.Bitmap;
+       import android.graphics.BitmapFactory;
+`}
+
+redef class TextureAsset
+
+       redef fun load_bitmap(asset_manager, path)
+       do
+               var stream = asset_manager.native_assets_manager.open(path.to_java_string)
+               return new NativeBitmap.from_stream_ex(stream, premultiply_alpha)
+       end
+end
+
+redef class NativeCByteArray
+
+       # The data was not premultiplied, don't unmultiply it
+       redef fun unmultiply(w, h) do end
+end
+
+redef class NativeBitmap
+
+       # Load from `input_stream` with optional `premultiply_alpha`
+       new from_stream_ex(input_stream: NativeInputStream, premultiply_alpha: Bool) in "Java" `{
+               BitmapFactory.Options opts = new BitmapFactory.Options();
+               opts.inPremultiplied = premultiply_alpha; // API 19
+               return BitmapFactory.decodeStream(input_stream, null, opts);
+       `}
+end
index 746a952..3b320f9 100644 (file)
@@ -31,12 +31,18 @@ class SmoothMaterial
        super Material
 
        # Ambient color, always visible
+       #
+       # The RGB values should be premultiplied by the alpha value.
        var ambient_color: Array[Float] is writable
 
        # Diffuse color when covered by a light source
+       #
+       # The RGB values should be premultiplied by the alpha value.
        var diffuse_color: Array[Float] is writable
 
        # Specular color affecting reflections
+       #
+       # The RGB values should be premultiplied by the alpha value.
        var specular_color: Array[Float] is writable
 
        redef fun draw(actor, model)
@@ -71,9 +77,13 @@ class SmoothMaterial
                program.camera.uniform(app.world_camera.position.x, app.world_camera.position.y, app.world_camera.position.z)
 
                # Colors from the material
-               program.ambient_color.uniform(ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3]*actor.alpha)
-               program.diffuse_color.uniform(diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3]*actor.alpha)
-               program.specular_color.uniform(specular_color[0], specular_color[1], specular_color[2], specular_color[3]*actor.alpha)
+               var a = actor.alpha
+               program.ambient_color.uniform(ambient_color[0]*a, ambient_color[1]*a,
+                                             ambient_color[2]*a, ambient_color[3]*a)
+               program.diffuse_color.uniform(diffuse_color[0]*a, diffuse_color[1]*a,
+                                             diffuse_color[2]*a, diffuse_color[3]*a)
+               program.specular_color.uniform(specular_color[0]*a, specular_color[1]*a,
+                                              specular_color[2]*a, specular_color[3]*a)
 
                # Execute draw
                if mesh.indices.is_empty then
@@ -188,9 +198,13 @@ class TexturedMaterial
 
                program.rotation.uniform new Matrix.gamnit_euler_rotation(actor.pitch, actor.yaw, actor.roll)
 
-               program.ambient_color.uniform(ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3]*actor.alpha)
-               program.diffuse_color.uniform(diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3]*actor.alpha)
-               program.specular_color.uniform(specular_color[0], specular_color[1], specular_color[2], specular_color[3]*actor.alpha)
+               var a = actor.alpha
+               program.ambient_color.uniform(ambient_color[0]*a, ambient_color[1]*a,
+                                             ambient_color[2]*a, ambient_color[3]*a)
+               program.diffuse_color.uniform(diffuse_color[0]*a, diffuse_color[1]*a,
+                                             diffuse_color[2]*a, diffuse_color[3]*a)
+               program.specular_color.uniform(specular_color[0]*a, specular_color[1]*a,
+                                              specular_color[2]*a, specular_color[3]*a)
 
                program.normal.array_enabled = true
                program.normal.array(mesh.normals, 3)
index 3c40ac1..bfe3392 100644 (file)
@@ -258,7 +258,6 @@ class ParticleProgram
                {
                        if (use_texture) {
                                gl_FragColor = texture2D(texture0, gl_PointCoord) * v_color;
-                               if (gl_FragColor.a <= 0.01) discard;
                        } else {
                                gl_FragColor = v_color;
                        }
@@ -301,7 +300,7 @@ class ExplosionProgram
                gl_Position = center * mvp;
                gl_PointSize = scale / gl_Position.z * pt;
 
-               if (pt > 0.8) v_color.a = (1.0-pt)/0.2;
+               if (pt > 0.8) v_color *= (1.0-pt)/0.2;
        """
 end
 
@@ -318,8 +317,8 @@ class SmokeProgram
                gl_PointSize = scale / gl_Position.z * (pt+0.1);
 
                if (pt < 0.1)
-                       v_color.a = pt / 0.1;
+                       v_color *= pt / 0.1;
                else
-                       v_color.a = 1.0 - pt*0.9;
+                       v_color *= 1.0 - pt*0.9;
        """
 end
index e9d1b4a..8e5566c 100644 (file)
@@ -50,19 +50,24 @@ end
 
 redef class TextureAsset
 
+       private fun load_bitmap(asset_manager: AssetManager, path: String): NativeBitmap
+       do
+               return asset_manager.bitmap(path)
+       end
+
        redef fun load_from_platform
        do
                jni_env.push_local_frame 4
 
                var asset_manager = app.asset_manager
-               var bmp = asset_manager.bitmap(path)
+               var bmp = load_bitmap(asset_manager, path)
                if bmp.is_java_null then
                        error = new Error("Failed to load texture at '{path}'")
                        jni_env.pop_local_frame
                        return
                end
 
-               var buf = bmp.copy_pixels
+               var buf = bmp.copy_pixels(unmultiply=not premultiply_alpha)
                loaded = true
                width = bmp.width.to_f
                height = bmp.height.to_f
index 56fd402..f3043c3 100644 (file)
@@ -430,7 +430,7 @@ redef class App
 
                # Enable blending
                gl.capabilities.blend.enable
-               glBlendFunc(gl_SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA)
+               glBlendFunc(gl_ONE, gl_ONE_MINUS_SRC_ALPHA)
 
                # Enable depth test
                gl.capabilities.depth_test.enable
@@ -695,7 +695,7 @@ private class Simple2dProgram
                        }
 
                        gl_Position = (vec4(c * scale, 1.0) * rotation() + translation)* mvp;
-                       v_color = color;
+                       v_color = vec4(color.rgb*color.a, color.a);
                }
                """ @ glsl_vertex_shader
 
index 65f654b..4fb98ac 100644 (file)
@@ -208,7 +208,7 @@ class CustomTexture
        end
 end
 
-# Texture with its own pixels
+# Texture with its own pixel data
 class RootTexture
        super Texture
 
@@ -221,6 +221,18 @@ class RootTexture
 
        init do all_root_textures.add self
 
+       # Should the pixels RGB values be premultiplied by their alpha value at loading?
+       #
+       # All gamnit textures must have premultiplied alpha, it provides a better
+       # alpha blending, avoids artifacts and allows for additive blending.
+       #
+       # When at `true`, the default, pixels RGB values are premultiplied
+       # at loading. Set to `false` if pixels RGB values are already
+       # premultiplied in the source data.
+       #
+       # This value must be set before calling `load`.
+       var premultiply_alpha = true is writable
+
        private fun load_from_pixels(pixels: Pointer, width, height: Int, format: GLPixelFormat)
        do
                var max_texture_size = glGetIntegerv(gl_MAX_TEXTURE_SIZE, 0)
@@ -229,6 +241,11 @@ class RootTexture
                        return
                end
 
+               # Premultiply alpha?
+               if premultiply_alpha and format == gl_RGBA then
+                       pixels.premultiply_alpha(width, height)
+               end
+
                glPixelStorei(gl_UNPACK_ALIGNEMENT, 1)
                var tex = glGenTextures(1)[0]
                gl_texture = tex
@@ -238,6 +255,9 @@ class RootTexture
 
                glHint(gl_GENERATE_MIPMAP_HINT, gl_NICEST)
                glGenerateMipmap(gl_TEXTURE_2D)
+               glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR_MIPMAP_LINEAR)
+
+               glBindTexture(gl_TEXTURE_2D, 0)
        end
 
        private fun load_checker(size: Int)
@@ -365,3 +385,20 @@ class TextureSet
        # Load all texture of this set
        fun load_all do for t in self do t.load
 end
+
+redef class Pointer
+       # Multiply RBG values by their alpha value
+       private fun premultiply_alpha(width, height: Int) `{
+               uint8_t *bytes = (uint8_t *)self;
+               int x, y, i = 0;
+               for(y = 0; y < height; y ++) {
+                       for(x = 0; x < width; x ++) {
+                               int a = bytes[i+3];
+                               bytes[i  ] = bytes[i  ] * a / 255;
+                               bytes[i+1] = bytes[i+1] * a / 255;
+                               bytes[i+2] = bytes[i+2] * a / 255;
+                               i += 4;
+                       }
+               }
+       `}
+end