From: Jean Privat Date: Wed, 28 Jun 2017 19:39:24 +0000 (-0400) Subject: Merge: gamnit: premultiply alpha for a nicer alpha blending X-Git-Url: http://nitlanguage.org?hp=ec0b87f7db3974a78ada6df4c34155295db86d8f Merge: gamnit: premultiply alpha for a nicer alpha blending 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 --- diff --git a/contrib/action_nitro/Makefile b/contrib/action_nitro/Makefile index b3fcebc..81db63b 100644 --- a/contrib/action_nitro/Makefile +++ b/contrib/action_nitro/Makefile @@ -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/ diff --git a/contrib/action_nitro/art/icon.svg b/contrib/action_nitro/art/icon.svg index 7383208..a0701e1 100644 --- a/contrib/action_nitro/art/icon.svg +++ b/contrib/action_nitro/art/icon.svg @@ -60,8 +60,8 @@ id="layer1" transform="translate(-286.05811,81.477047)"> 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 diff --git a/contrib/model_viewer/src/globe.nit b/contrib/model_viewer/src/globe.nit index a5a9a62..12c6ffc 100644 --- a/contrib/model_viewer/src/globe.nit +++ b/contrib/model_viewer/src/globe.nit @@ -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 index 0000000..3325ece --- /dev/null +++ b/lib/gamnit/android19.nit @@ -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 diff --git a/lib/gamnit/depth/more_materials.nit b/lib/gamnit/depth/more_materials.nit index 746a952..3b320f9 100644 --- a/lib/gamnit/depth/more_materials.nit +++ b/lib/gamnit/depth/more_materials.nit @@ -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) diff --git a/lib/gamnit/depth/particles.nit b/lib/gamnit/depth/particles.nit index 3c40ac1..bfe3392 100644 --- a/lib/gamnit/depth/particles.nit +++ b/lib/gamnit/depth/particles.nit @@ -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 diff --git a/lib/gamnit/display_android.nit b/lib/gamnit/display_android.nit index e9d1b4a..8e5566c 100644 --- a/lib/gamnit/display_android.nit +++ b/lib/gamnit/display_android.nit @@ -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 diff --git a/lib/gamnit/flat.nit b/lib/gamnit/flat.nit index 56fd402..f3043c3 100644 --- a/lib/gamnit/flat.nit +++ b/lib/gamnit/flat.nit @@ -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 diff --git a/lib/gamnit/textures.nit b/lib/gamnit/textures.nit index 65f654b..4fb98ac 100644 --- a/lib/gamnit/textures.nit +++ b/lib/gamnit/textures.nit @@ -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