Skip to content

Black square glitch on latest version of filament #9433

@vbod-dm

Description

@vbod-dm

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

Image

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 only

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions