Last active
July 26, 2023 23:02
-
-
Save victorbrndls/420d2c73cfff992999ab83dfdf8b962d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package io.opitas.moves.core.rendering.filament | |
import android.view.MotionEvent | |
import android.view.Surface | |
import android.view.TextureView | |
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.Fence | |
import com.google.android.filament.LightManager | |
import com.google.android.filament.Renderer | |
import com.google.android.filament.Scene | |
import com.google.android.filament.SwapChain | |
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 com.google.android.filament.gltfio.Animator | |
import com.google.android.filament.gltfio.AssetLoader | |
import com.google.android.filament.gltfio.FilamentAsset | |
import com.google.android.filament.gltfio.MaterialProvider | |
import com.google.android.filament.gltfio.ResourceLoader | |
import com.google.android.filament.gltfio.UbershaderProvider | |
import com.google.android.filament.utils.Float3 | |
import com.google.android.filament.utils.Manipulator | |
import com.google.android.filament.utils.Mat4 | |
import com.google.android.filament.utils.max | |
import com.google.android.filament.utils.scale | |
import com.google.android.filament.utils.translation | |
import com.google.android.filament.utils.transpose | |
import io.opitas.moves.core.log.MovesLogger | |
import io.opitas.moves.core.rendering.filament.gesture.CustomizableGestureDetector | |
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.Job | |
import kotlinx.coroutines.withContext | |
import java.nio.Buffer | |
private const val kNearPlane = 0.05f // 5 cm | |
private const val kFarPlane = 1000.0f // 1 km | |
private const val kAperture = 16f | |
private const val kShutterSpeed = 1f / 125f | |
private const val kSensitivity = 100f | |
class ModelViewer( | |
val engine: Engine, | |
private val uiHelper: UiHelper, | |
) : android.view.View.OnTouchListener { | |
var asset: FilamentAsset? = null | |
private set | |
var animator: Animator? = null | |
private set | |
@Suppress("unused") | |
val progress | |
get() = resourceLoader.asyncGetLoadProgress() | |
var normalizeSkinningWeights = true | |
var cameraFocalLength = 56f | |
set(value) { | |
field = value | |
updateCameraProjection() | |
} | |
var cameraNear = kNearPlane | |
set(value) { | |
field = value | |
updateCameraProjection() | |
} | |
var cameraFar = kFarPlane | |
set(value) { | |
field = value | |
updateCameraProjection() | |
} | |
val scene: Scene | |
val view: View | |
val camera: Camera | |
val renderer: Renderer | |
@Entity | |
val light: Int | |
private lateinit var displayHelper: DisplayHelper | |
private lateinit var cameraManipulator: Manipulator | |
private lateinit var gestureDetector: CustomizableGestureDetector | |
private var textureView: TextureView? = null | |
private var fetchResourcesJob: Job? = null | |
private var swapChain: SwapChain? = null | |
private var assetLoader: AssetLoader | |
private var materialProvider: MaterialProvider | |
private var resourceLoader: ResourceLoader | |
private val readyRenderables = IntArray(128) // add up to 128 entities at a time | |
var cameraManipulatorEnabled = false | |
private val eyePos = DoubleArray(3) | |
private val target = DoubleArray(3) | |
private val upward = DoubleArray(3) | |
init { | |
renderer = engine.createRenderer() | |
scene = engine.createScene() | |
camera = engine.createCamera(engine.entityManager.create()) | |
.apply { setExposure(kAperture, kShutterSpeed, kSensitivity) } | |
view = engine.createView() | |
view.scene = scene | |
view.camera = camera | |
materialProvider = UbershaderProvider(engine) | |
assetLoader = AssetLoader(engine, materialProvider, EntityManager.get()) | |
resourceLoader = ResourceLoader(engine, normalizeSkinningWeights) | |
// Always add a direct light source since it is required for shadowing. | |
// We highly recommend adding an indirect light as well. | |
light = EntityManager.get().create() | |
val (r, g, b) = Colors.cct(6_500.0f) | |
LightManager.Builder(LightManager.Type.DIRECTIONAL) | |
.color(r, g, b) | |
.intensity(100_000.0f) | |
.direction(0.0f, -1.0f, 0.0f) | |
.castShadows(true) | |
.build(engine, light) | |
scene.addEntity(light) | |
} | |
constructor( | |
textureView: TextureView, | |
engine: Engine = Engine.create(), | |
uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK), | |
manipulator: Manipulator? = null, | |
) : this(engine, uiHelper) { | |
cameraManipulator = manipulator ?: Manipulator.Builder() | |
.targetPosition( | |
kDefaultObjectPosition.x, | |
kDefaultObjectPosition.y, | |
kDefaultObjectPosition.z | |
) | |
.viewport(textureView.width, textureView.height) | |
.build(Manipulator.Mode.ORBIT) | |
MovesLogger.i("Rendering backend: ${engine.backend}") | |
this.textureView = textureView | |
gestureDetector = CustomizableGestureDetector(textureView, cameraManipulator) | |
displayHelper = DisplayHelper(textureView.context) | |
uiHelper.renderCallback = SurfaceCallback() | |
uiHelper.attachTo(textureView) | |
addDetachListener(textureView) | |
} | |
/** | |
* Loads a monolithic binary glTF and populates the Filament scene. | |
*/ | |
fun loadModelGlb(buffer: Buffer) { | |
destroyModel() | |
asset = assetLoader.createAsset(buffer) | |
asset?.let { asset -> | |
resourceLoader.asyncBeginLoad(asset) | |
animator = asset.getInstance().animator | |
asset.releaseSourceData() | |
} | |
} | |
/** | |
* Loads a JSON-style glTF file and populates the Filament scene. | |
* | |
* The given callback is triggered for each requested resource. | |
*/ | |
fun loadModelGltf(buffer: Buffer, callback: (String) -> Buffer?) { | |
destroyModel() | |
asset = assetLoader.createAsset(buffer) | |
asset?.let { asset -> | |
for (uri in asset.resourceUris) { | |
val resourceBuffer = callback(uri) | |
if (resourceBuffer == null) { | |
this.asset = null | |
return | |
} | |
resourceLoader.addResourceData(uri, resourceBuffer) | |
} | |
resourceLoader.asyncBeginLoad(asset) | |
animator = asset.getInstance().animator | |
asset.releaseSourceData() | |
} | |
} | |
/** | |
* Sets up a root transform on the current model to make it fit into a unit cube. | |
* | |
* @param centerPoint Coordinate of center point of unit cube, defaults to < 0, 0, -4 > | |
*/ | |
fun transformToUnitCube(centerPoint: Float3 = kDefaultObjectPosition) { | |
asset?.let { asset -> | |
val tm = engine.transformManager | |
var center = asset.boundingBox.center.let { v -> Float3(v[0], v[1], v[2]) } | |
val halfExtent = asset.boundingBox.halfExtent.let { v -> Float3(v[0], v[1], v[2]) } | |
val maxExtent = 2.0f * max(halfExtent) | |
val scaleFactor = 2.0f / maxExtent | |
center -= centerPoint / scaleFactor | |
val transform = scale(Float3(scaleFactor)) * translation(-center) | |
tm.setTransform(tm.getInstance(asset.root), transpose(transform).toFloatArray()) | |
} | |
} | |
/** | |
* Removes the transformation that was set up via transformToUnitCube. | |
*/ | |
fun clearRootTransform() { | |
asset?.let { | |
val tm = engine.transformManager | |
tm.setTransform(tm.getInstance(it.root), Mat4().toFloatArray()) | |
} | |
} | |
/** | |
* Frees all entities associated with the most recently-loaded model. | |
*/ | |
fun destroyModel() { | |
fetchResourcesJob?.cancel() | |
resourceLoader.asyncCancelLoad() | |
resourceLoader.evictResourceData() | |
asset?.let { asset -> | |
this.scene.removeEntities(asset.entities) | |
assetLoader.destroyAsset(asset) | |
this.asset = null | |
this.animator = null | |
} | |
} | |
/** | |
* Renders the model and updates the Filament camera. | |
* | |
* @param frameTimeNanos time in nanoseconds when the frame started being rendered, | |
* typically comes from {@link android.view.Choreographer.FrameCallback} | |
*/ | |
fun render(frameTimeNanos: Long) { | |
if (!uiHelper.isReadyToRender) { | |
return | |
} | |
// Allow the resource loader to finalize textures that have become ready. | |
resourceLoader.asyncUpdateLoad() | |
// Add renderable entities to the scene as they become ready. | |
asset?.let { populateScene(it) } | |
if (cameraManipulatorEnabled) { | |
cameraManipulator.getLookAt(eyePos, target, upward) | |
camera.lookAt( | |
eyePos[0], eyePos[1], eyePos[2], | |
target[0], target[1], target[2], | |
upward[0], upward[1], upward[2] | |
) | |
} | |
// Render the scene, unless the renderer wants to skip the frame. | |
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) { | |
renderer.render(view) | |
renderer.endFrame() | |
} | |
} | |
fun lookAt( | |
eyePos: DoubleArray, | |
target: DoubleArray, | |
upward: DoubleArray, | |
) { | |
camera.lookAt( | |
eyePos[0], eyePos[1], eyePos[2], | |
target[0], target[1], target[2], | |
upward[0], upward[1], upward[2] | |
) | |
} | |
private fun populateScene(asset: FilamentAsset) { | |
val rcm = engine.renderableManager | |
var count = 0 | |
val popRenderables = { count = asset.popRenderables(readyRenderables); count != 0 } | |
while (popRenderables()) { | |
for (i in 0..count - 1) { | |
val ri = rcm.getInstance(readyRenderables[i]) | |
rcm.setScreenSpaceContactShadows(ri, true) | |
} | |
scene.addEntities(readyRenderables.take(count).toIntArray()) | |
} | |
scene.addEntities(asset.lightEntities) | |
} | |
private fun addDetachListener(view: android.view.View) { | |
view.addOnAttachStateChangeListener(object : android.view.View.OnAttachStateChangeListener { | |
override fun onViewAttachedToWindow(v: android.view.View) {} | |
override fun onViewDetachedFromWindow(v: android.view.View) { | |
MovesLogger.i("Destroying ModelViewer") | |
uiHelper.detach() | |
destroyModel() | |
assetLoader.destroy() | |
materialProvider.destroyMaterials() | |
materialProvider.destroy() | |
resourceLoader.destroy() | |
engine.destroyEntity(light) | |
engine.destroyRenderer(renderer) | |
engine.destroyView(this@ModelViewer.view) | |
engine.destroyScene(scene) | |
engine.destroyCameraComponent(camera.entity) | |
EntityManager.get().destroy(camera.entity) | |
EntityManager.get().destroy(light) | |
//engine.destroy() | |
} | |
}) | |
} | |
/** | |
* Handles a [MotionEvent] to enable one-finger orbit, two-finger pan, and pinch-to-zoom. | |
*/ | |
fun onTouchEvent(event: MotionEvent) { | |
gestureDetector.onTouchEvent(event) | |
} | |
@SuppressWarnings("ClickableViewAccessibility") | |
override fun onTouch(view: android.view.View, event: MotionEvent): Boolean { | |
onTouchEvent(event) | |
return true | |
} | |
fun setEnabledGestures(gestures: Set<CustomizableGestureDetector.Gesture>) { | |
gestureDetector.setEnabledGestures(gestures) | |
} | |
private suspend fun fetchResources(asset: FilamentAsset, callback: (String) -> Buffer) { | |
val items = HashMap<String, Buffer>() | |
val resourceUris = asset.resourceUris | |
for (resourceUri in resourceUris) { | |
items[resourceUri] = callback(resourceUri) | |
} | |
withContext(Dispatchers.Main) { | |
for ((uri, buffer) in items) { | |
resourceLoader.addResourceData(uri, buffer) | |
} | |
resourceLoader.asyncBeginLoad(asset) | |
animator = asset.getInstance().animator | |
asset.releaseSourceData() | |
} | |
} | |
private fun updateCameraProjection() { | |
val width = view.viewport.width | |
val height = view.viewport.height | |
val aspect = width.toDouble() / height.toDouble() | |
camera.setLensProjection( | |
cameraFocalLength.toDouble(), aspect, | |
cameraNear.toDouble(), cameraFar.toDouble() | |
) | |
} | |
inner class SurfaceCallback : UiHelper.RendererCallback { | |
override fun onNativeWindowChanged(surface: Surface) { | |
swapChain?.let { engine.destroySwapChain(it) } | |
swapChain = engine.createSwapChain(surface) | |
textureView?.let { displayHelper.attach(renderer, it.display) } | |
} | |
override fun onDetachedFromSurface() { | |
displayHelper.detach() | |
swapChain?.let { | |
engine.destroySwapChain(it) | |
engine.flushAndWait() | |
swapChain = null | |
} | |
} | |
override fun onResized(width: Int, height: Int) { | |
view.viewport = Viewport(0, 0, width, height) | |
cameraManipulator.setViewport(width, height) | |
updateCameraProjection() | |
synchronizePendingFrames(engine) | |
} | |
} | |
private fun synchronizePendingFrames(engine: Engine) { | |
// Wait for all pending frames to be processed before returning. This is to | |
// avoid a race between the surface being resized before pending frames are | |
// rendered into it. | |
val fence = engine.createFence() | |
fence.wait(Fence.Mode.FLUSH, Fence.WAIT_FOR_EVER) | |
engine.destroyFence(fence) | |
} | |
companion object { | |
private val kDefaultObjectPosition = Float3(0.0f, 0.0f, -4.0f) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment