-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Open
Labels
androidIssue/feature request for Android onlyIssue/feature request for Android only
Description
Describe the bug
I’ve updated filament from 1.32.2 to 1.66.1 and I noticed an issue on my Samsung note 10+ with my glb.
There some black square that appears when 2 "shadingModel: lit" materials collide.
To Reproduce
The minimal way to reproduce it is :
- have a directional light
- 2 materials:
name : lit,
shadingModel : lit,
refractionMode : screenspace,
refractionType : solid,
reflections : default
}
fragment {
void material(inout MaterialInputs material) {
material.baseColor = float4(1.0, 1.0, 1.0, 1.0);
material.metallic = 0.0;
material.roughness = 0.2;
material.transmission = 0.5;
material.ior = 1.15;
prepareMaterial(material);
}
} material {
name : lit2,
shadingModel : lit,
refractionMode : none,
}
fragment {
void material(inout MaterialInputs material) {
material.metallic = 0.0;
material.roughness = 0.06;
material.ior = 1.5;
prepareMaterial(material);
material.baseColor = float4(1.0, 1.0, 1.0, 1.0);
}
}- Two ovale shapes imbricated
Expected behavior
No black square on the shape as it was in 1.32.2.
Screenshots
Logs
If applicable, copy full logs from your console here. Please do not
use screenshots of logs, copy them as text, use gist or attach an uncompressed file.
**Smartphone **
- Device: Samsung galaxy note 10+
- OS: Android 12, One UI 4.1
Additional context
I used the sample 'sample-material-instance-stress' as a base to do the mininum reproductible setup, here the code
MainActivity.kt
/*
* Copyright (C) 2025 The Android Open Source Project
*
* 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.
*/
package com.google.android.filament.materialinstancestress
import android.animation.ValueAnimator
import android.app.Activity
import android.opengl.Matrix
import android.os.Bundle
import android.view.Choreographer
import android.view.Surface
import android.view.SurfaceView
import com.google.android.filament.Box
import com.google.android.filament.Camera
import com.google.android.filament.Colors
import com.google.android.filament.Engine
import com.google.android.filament.Entity
import com.google.android.filament.EntityManager
import com.google.android.filament.Filament
import com.google.android.filament.IndexBuffer
import com.google.android.filament.LightManager
import com.google.android.filament.Material
import com.google.android.filament.MaterialInstance
import com.google.android.filament.RenderableManager
import com.google.android.filament.RenderableManager.PrimitiveType
import com.google.android.filament.Renderer
import com.google.android.filament.Scene
import com.google.android.filament.SwapChain
import com.google.android.filament.VertexBuffer
import com.google.android.filament.View
import com.google.android.filament.Viewport
import com.google.android.filament.android.DisplayHelper
import com.google.android.filament.android.UiHelper
import java.nio.ByteBuffer
import java.nio.channels.Channels
import kotlin.math.cos
import kotlin.math.sin
class MainActivity : Activity() {
// Make sure to initialize Filament first
// This loads the JNI library needed by most API calls
companion object {
init {
Filament.init()
}
private const val NUM_CUBES = 2 // 1 central + 8 surrounding
private const val CIRCLE_RADIUS = 3.0f
}
// The View we want to render into
private lateinit var surfaceView: SurfaceView
// UiHelper is provided by Filament to manage SurfaceView and SurfaceTexture
private lateinit var uiHelper: UiHelper
// DisplayHelper is provided by Filament to manage the display
private lateinit var displayHelper: DisplayHelper
// Choreographer is used to schedule new frames
private lateinit var choreographer: Choreographer
// Engine creates and destroys Filament resources
// Each engine must be accessed from a single thread of your choosing
// Resources cannot be shared across engines
private lateinit var engine: Engine
// A renderer instance is tied to a single surface (SurfaceView, TextureView, etc.)
private lateinit var renderer: Renderer
// A scene holds all the renderable, lights, etc. to be drawn
private lateinit var scene: Scene
// A view defines a viewport, a scene and a camera for rendering
private lateinit var view: View
// Should be pretty obvious :)
private lateinit var camera: Camera
private lateinit var material: Material
private lateinit var material2: Material
private lateinit var vertexBuffer: VertexBuffer
private lateinit var indexBuffer: IndexBuffer
private var indexCount: Int = 0
// Filament entity representing a renderable object
@Entity private val renderables = IntArray(NUM_CUBES)
private val materialInstances = arrayOfNulls<MaterialInstance>(NUM_CUBES)
@Entity private val lights = mutableListOf<Int>()
// A swap chain is Filament's representation of a surface
private var swapChain: SwapChain? = null
private var cameraRadius = 42.7f // Initial distance (calculated from original 15Y, 40Z)
private var cameraTheta = 0.0f // Azimuth (horizontal) in radians
private var cameraPhi = 0.358f // Elevation (vertical) in radians (from original 15Y, 40Z)
private var mLastX = 0.0f
private var mLastY = 0.0f
private val DRAG_SPEED = 0.005f
// Performs the rendering and schedules new frames
private val frameScheduler = FrameCallback()
private val animator = ValueAnimator.ofFloat(0.0f, 360.0f)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
surfaceView = SurfaceView(this)
setContentView(surfaceView)
surfaceView.setOnTouchListener(touchListener)
choreographer = Choreographer.getInstance()
displayHelper = DisplayHelper(this)
setupSurfaceView()
setupFilament()
setupView()
setupScene()
}
private fun setupSurfaceView() {
uiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK)
uiHelper.renderCallback = SurfaceCallback()
uiHelper.attachTo(surfaceView)
}
private fun setupFilament() {
engine = Engine.create()
renderer = engine.createRenderer()
scene = engine.createScene()
view = engine.createView()
camera = engine.createCamera(engine.entityManager.create())
renderer.clearOptions = Renderer.ClearOptions().apply {
clearColor = floatArrayOf(0.1f, 0.2f, 0.3f, 1.0f) // Dark blue
clear = true
}
}
private fun setupView() {
view.camera = camera
view.scene = scene
}
private fun setupScene() {
loadMaterial()
createMesh()
val tcm = engine.transformManager
for (i in 0 until NUM_CUBES) {
val posX: Float
val posY: Float
val posZ: Float
val scaleX: Float
val scaleY: Float
val scaleZ: Float
if (i == 0) {
// Central rugby ball
posX = 0.0f
posY = 0.0f
posZ = 0.0f
scaleX = 5.5f
scaleY = 3.0f
scaleZ = 3.0f
} else {
// Surrounding rugby balls
val numSurrounding = NUM_CUBES - 1
val angle = (i - 1) * 2.0 * Math.PI / numSurrounding
posX = CIRCLE_RADIUS * cos(angle).toFloat()
posY = 0.0f
posZ = CIRCLE_RADIUS * sin(angle).toFloat()
scaleX = 2.75f
scaleY = 1.5f
scaleZ = 1.5f
}
// Create material instance for this ball
if (i % 2 == 0) {
materialInstances[i] = material.createInstance()
} else {
materialInstances[i] = material2.createInstance()
}
// Create the renderable entity
renderables[i] = EntityManager.get().create()
// Create the renderable component
RenderableManager.Builder(1)
.boundingBox(Box(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f))
.geometry(0, PrimitiveType.TRIANGLES, vertexBuffer, indexBuffer, 0, indexCount)
.material(0, materialInstances[i]!!)
.build(engine, renderables[i])
// Add to scene
scene.addEntity(renderables[i])
// 1. Create a scale matrix
val scaleMatrix = FloatArray(16)
Matrix.setIdentityM(scaleMatrix, 0)
Matrix.scaleM(scaleMatrix, 0, scaleX, scaleY, scaleZ)
// 2. Create a translation matrix
val translationMatrix = FloatArray(16)
Matrix.setIdentityM(translationMatrix, 0)
Matrix.translateM(translationMatrix, 0, posX, posY, posZ)
// 3. Multiply to get the final transform: T * S
val finalTransform = FloatArray(16)
Matrix.multiplyMM(finalTransform, 0, translationMatrix, 0, scaleMatrix, 0)
// Get the transform component instance and set the transform
val inst = tcm.getInstance(renderables[i])
tcm.setTransform(inst, finalTransform)
}
// Directional light
lights.add(EntityManager.get().create())
val (r, g, b) = Colors.cct(5_500.0f)
LightManager.Builder(LightManager.Type.DIRECTIONAL)
.color(r, g, b)
.intensity(110_000.0f)
.direction(0.0f, -0.5f, -1.0f)
.castShadows(true)
.build(engine, lights.last())
scene.addEntity(lights.last())
camera.setExposure(16.0f, 1.0f / 125.0f, 100.0f)
updateCamera()
}
private fun loadMaterial() {
readUncompressedAsset("materials/lit.filamat").let {
material = Material.Builder().payload(it, it.remaining()).build(engine)
}
readUncompressedAsset("materials/lit2.filamat").let {
material2 = Material.Builder().payload(it, it.remaining()).build(engine)
}
}
private fun createMesh() {
val sphere = PrimitiveFactory.createSphere(engine, 1.0f, 32, 32)
vertexBuffer = sphere.vertexBuffer
indexBuffer = sphere.indexBuffer
indexCount = sphere.indexCount
}
private fun updateCamera() {
// Calculate eye position from spherical coordinates
val eyeY = cameraRadius * sin(cameraPhi)
val horizontalRadius = cameraRadius * cos(cameraPhi)
val eyeX = horizontalRadius * sin(cameraTheta)
val eyeZ = horizontalRadius * cos(cameraTheta)
// Point the camera at the center (0,0,0)
camera.lookAt(eyeX.toDouble(), eyeY.toDouble(), eyeZ.toDouble(), // eye
0.0, 0.0, 0.0, // center
0.0, 1.0, 0.0) // up
}
private val touchListener = object : android.view.View.OnTouchListener {
override fun onTouch(v: android.view.View?, event: android.view.MotionEvent?): Boolean {
if (event == null) return false
when (event.action) {
android.view.MotionEvent.ACTION_DOWN -> {
mLastX = event.x
mLastY = event.y
return true
}
android.view.MotionEvent.ACTION_MOVE -> {
val dx = event.x - mLastX
val dy = event.y - mLastY
cameraTheta -= dx * DRAG_SPEED
cameraPhi += dy * DRAG_SPEED
// Clamp vertical angle to avoid flipping over
cameraPhi = cameraPhi.coerceIn(-1.5f, 1.5f)
updateCamera()
mLastX = event.x
mLastY = event.y
return true
}
}
return v?.onTouchEvent(event) ?: false
}
}
override fun onResume() {
super.onResume()
choreographer.postFrameCallback(frameScheduler)
animator.start()
}
override fun onPause() {
super.onPause()
choreographer.removeFrameCallback(frameScheduler)
animator.cancel()
}
override fun onDestroy() {
super.onDestroy()
// Stop the animation and any pending frame
choreographer.removeFrameCallback(frameScheduler)
animator.cancel();
// Always detach the surface before destroying the engine
uiHelper.detach()
// Cleanup all resources
lights.forEach { engine.destroyEntity(it) }
// Destroy all entities and material instances
for (i in 0 until NUM_CUBES) {
engine.destroyEntity(renderables[i])
materialInstances[i]?.let { engine.destroyMaterialInstance(it) }
}
engine.destroyRenderer(renderer)
engine.destroyVertexBuffer(vertexBuffer)
engine.destroyIndexBuffer(indexBuffer)
engine.destroyMaterial(material)
engine.destroyMaterial(material2)
engine.destroyView(view)
engine.destroyScene(scene)
engine.destroyCameraComponent(camera.entity)
// Engine.destroyEntity() destroys Filament related resources only
// (components), not the entity itself
val entityManager = EntityManager.get()
lights.forEach { entityManager.destroy(it) }
for (entity in renderables) {
entityManager.destroy(entity)
}
entityManager.destroy(camera.entity)
// Destroying the engine will free up any resource you may have forgotten
// to destroy, but it's recommended to do the cleanup properly
engine.destroy()
}
inner class FrameCallback : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
// Schedule the next frame
choreographer.postFrameCallback(this)
// This check guarantees that we have a swap chain
if (uiHelper.isReadyToRender) {
// If beginFrame() returns false you should skip the frame
// This means you are sending frames too quickly to the GPU
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) {
renderer.render(view)
renderer.endFrame()
}
}
}
}
inner class SurfaceCallback : UiHelper.RendererCallback {
override fun onNativeWindowChanged(surface: Surface) {
swapChain?.let { engine.destroySwapChain(it) }
swapChain = engine.createSwapChain(surface)
displayHelper.attach(renderer, surfaceView.display)
}
override fun onDetachedFromSurface() {
displayHelper.detach()
swapChain?.let {
engine.destroySwapChain(it)
// Required to ensure we don't return before Filament is done executing the
// destroySwapChain command, otherwise Android might destroy the Surface
// too early
engine.flushAndWait()
swapChain = null
}
}
override fun onResized(width: Int, height: Int) {
val aspect = width.toDouble() / height.toDouble()
camera.setProjection(45.0, aspect, 0.1, 100.0, Camera.Fov.VERTICAL)
view.viewport = Viewport(0, 0, width, height)
}
}
private fun readUncompressedAsset(assetName: String): ByteBuffer {
assets.openFd(assetName).use { fd ->
val input = fd.createInputStream()
val dst = ByteBuffer.allocate(fd.length.toInt())
val src = Channels.newChannel(input)
src.read(dst)
src.close()
return dst.apply { rewind() }
}
}
}PrimitiveFactory.kt
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*/
package com.google.android.filament.materialinstancestress
import com.google.android.filament.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
object PrimitiveFactory {
data class Primitive(
val vertexBuffer: VertexBuffer,
val indexBuffer: IndexBuffer,
val indexCount: Int
)
fun createSphere(engine: Engine, radius: Float, rings: Int, sectors: Int): Primitive {
val floatSize = 4
val shortSize = 2
// Each vertex has 3 floats for position and 4 floats for the tangent frame
val vertexSize = (3 + 4) * floatSize
val vertexCount = (rings + 1) * (sectors + 1)
val indexCount = rings * sectors * 6
val vertexData = ByteBuffer.allocate(vertexCount * vertexSize).order(ByteOrder.nativeOrder())
val indexData = ByteBuffer.allocate(indexCount * shortSize).order(ByteOrder.nativeOrder())
val R = 1.0f / rings.toFloat()
val S = 1.0f / sectors.toFloat()
for (r in 0..rings) {
for (s in 0..sectors) {
val y = sin(-PI / 2.0 + PI * r * R).toFloat()
val x = (cos(2.0 * PI * s * S) * sin(PI * r * R)).toFloat()
val z = (sin(2.0 * PI * s * S) * sin(PI * r * R)).toFloat()
// Position
vertexData.putFloat(x * radius)
vertexData.putFloat(y * radius)
vertexData.putFloat(z * radius)
// Normal (for a sphere, the normal is the normalized position)
val nx = x
val ny = y
val nz = z
// Tangent (derivative with respect to longitude)
val tx = (-sin(2.0 * PI * s * S)).toFloat()
val ty = 0.0f
val tz = (cos(2.0 * PI * s * S)).toFloat()
// Bitangent is cross product of normal and tangent
val btx = ny * tz - nz * ty
val bty = nz * tx - nx * tz
val btz = nx * ty - ny * tx
val tangentFrame = FloatArray(4)
MathUtils.packTangentFrame(nx, ny, nz, tx, ty, tz, btx, bty, btz, tangentFrame)
vertexData.putFloat(tangentFrame[0])
vertexData.putFloat(tangentFrame[1])
vertexData.putFloat(tangentFrame[2])
vertexData.putFloat(tangentFrame[3])
}
}
for (r in 0 until rings) {
for (s in 0 until sectors) {
val first = (r * (sectors + 1) + s).toShort()
val second = (r * (sectors + 1) + (s + 1)).toShort()
val third = ((r + 1) * (sectors + 1) + s).toShort()
val fourth = ((r + 1) * (sectors + 1) + (s + 1)).toShort()
indexData.putShort(first)
indexData.putShort(third)
indexData.putShort(second)
indexData.putShort(second)
indexData.putShort(third)
indexData.putShort(fourth)
}
}
val vertexBuffer = VertexBuffer.Builder()
.vertexCount(vertexCount)
.bufferCount(1)
.attribute(VertexBuffer.VertexAttribute.POSITION, 0, VertexBuffer.AttributeType.FLOAT3, 0, vertexSize)
.attribute(VertexBuffer.VertexAttribute.TANGENTS, 0, VertexBuffer.AttributeType.FLOAT4, 3 * floatSize, vertexSize)
.build(engine)
val indexBuffer = IndexBuffer.Builder()
.indexCount(indexCount)
.bufferType(IndexBuffer.Builder.IndexType.USHORT)
.build(engine)
vertexBuffer.setBufferAt(engine, 0, vertexData.flip())
indexBuffer.setBuffer(engine, indexData.flip())
return Primitive(vertexBuffer, indexBuffer, indexCount)
}
}Metadata
Metadata
Assignees
Labels
androidIssue/feature request for Android onlyIssue/feature request for Android only