-
-
Save ThomasGorisse/0f144575e25405feaceac09065384bad to your computer and use it in GitHub Desktop.
| package io.github.sceneview | |
| import android.annotation.SuppressLint | |
| import android.content.Context | |
| import android.content.res.AssetManager | |
| import android.graphics.PixelFormat | |
| import android.graphics.drawable.ColorDrawable | |
| import android.media.MediaRecorder | |
| import android.os.Handler | |
| import android.os.Looper | |
| import android.util.AttributeSet | |
| import android.view.* | |
| import android.widget.FrameLayout | |
| import androidx.activity.ComponentActivity | |
| import androidx.core.content.getSystemService | |
| import androidx.fragment.app.Fragment | |
| import androidx.fragment.app.findFragment | |
| import androidx.lifecycle.* | |
| import com.google.android.filament.* | |
| import com.google.android.filament.View | |
| import com.google.android.filament.View.* | |
| import com.google.android.filament.android.DisplayHelper | |
| import com.google.android.filament.android.UiHelper | |
| import com.google.android.filament.gltfio.* | |
| import com.google.android.filament.utils.* | |
| import com.gorisse.thomas.lifecycle.getActivity | |
| import com.gorisse.thomas.lifecycle.observe | |
| import io.github.sceneview.components.NodeManager | |
| import io.github.sceneview.environment.IBLPrefilter | |
| import io.github.sceneview.environment.loadIndirectLight | |
| import io.github.sceneview.gesture.* | |
| import io.github.sceneview.gesture.GestureDetector | |
| import io.github.sceneview.math.Position | |
| import io.github.sceneview.model.* | |
| import io.github.sceneview.node.CameraNode | |
| import io.github.sceneview.node.LightNode | |
| import io.github.sceneview.node.ModelNode | |
| import io.github.sceneview.node.Node | |
| import io.github.sceneview.scene.* | |
| import io.github.sceneview.utils.* | |
| typealias Entity = Int | |
| typealias CameraGestureDetector = com.google.android.filament.utils.GestureDetector | |
| typealias CameraManipulator = Manipulator | |
| const val kIblLocation = "environments/default" | |
| private const val kNearPlane = 0.05 // 5 cm | |
| private const val kFarPlane = 1000.0 // 1 km | |
| private const val kAperture = 16f | |
| private const val kShutterSpeed = 1f / 125f | |
| private const val kSensitivity = 100f | |
| /** | |
| * ### A SurfaceView that manages rendering and interactions with the 3D scene. | |
| * | |
| * Maintains the scene graph, a hierarchical organization of a scene's content. | |
| * A scene can have zero or more child nodes and each node can have zero or more child nodes. | |
| * The Scene also provides hit testing, a way to detect which node is touched by a MotionEvent or | |
| * Ray. | |
| * | |
| * @property engine Engine creates and destroys Filament resources. | |
| * Each engine must be accessed from a single thread of your choosing. | |
| * Resources cannot be shared across engines. | |
| * @property sharedNodeManager Provide your own instance if you want to share [Node] instances | |
| * between multiple views. | |
| * @property UiHelper Provided by Filament to manage SurfaceView and SurfaceTexture | |
| * To choose a specific rendering resolution, add the following line: | |
| * `uiHelper.setDesiredSize(1280, 720)` | |
| */ | |
| open class SceneView @JvmOverloads constructor( | |
| context: Context, | |
| attrs: AttributeSet? = null, | |
| defStyleAttr: Int = 0, | |
| defStyleRes: Int = 0, | |
| val sharedEngine: Engine? = null, | |
| val sharedModelLoader: ModelLoader? = null, | |
| val sharedNodeManager: NodeManager? = null, | |
| val uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK), | |
| camera: CameraNode? = null, | |
| var isCameraManipulatorEnabled: Boolean = true, | |
| cameraManipulator: CameraManipulator? = null, | |
| nodesManipulator: NodesManipulator? = null | |
| ) : SurfaceView(context, attrs, defStyleAttr, defStyleRes) { | |
| /** | |
| * DisplayHelper is provided by Filament to manage the display | |
| */ | |
| var displayHelper: DisplayHelper | |
| val engine: Engine | |
| var normalizeSkinningWeights = true | |
| var cameraFocalLength = 28f | |
| set(value) { | |
| field = value | |
| updateCameraProjection() | |
| } | |
| val nodeManager: NodeManager | |
| var modelLoader: ModelLoader | |
| var iblPrefilter: IBLPrefilter | |
| val transformManager get() = engine.transformManager | |
| val renderableManager get() = engine.renderableManager | |
| val lightManager get() = engine.lightManager | |
| val scene: Scene | |
| val view: View | |
| val renderer: Renderer | |
| val surfaceMirorer: SurfaceMirorer | |
| val cameraManipulator: CameraManipulator | |
| val nodesManipulator: NodesManipulator | |
| lateinit var gestureDetector: GestureDetector | |
| /** | |
| * The View's Camera. | |
| * | |
| * Associates the specified Camera with this View. A Camera can be associated with several | |
| * View instances. To remove an existing association, simply pass null. | |
| * The View does not take ownership of the Scene pointer. Before destroying a Camera, be sure | |
| * to remove it from all associated Views. | |
| */ | |
| var cameraNode: CameraNode? = null | |
| private set(value) { | |
| field?.let { removeChildNode(it) } | |
| field = value | |
| view.camera = value?.camera | |
| value?.let { addChildNode(it) } | |
| } | |
| val camera get() = cameraNode?.camera | |
| /** | |
| * Always add a direct light source since it is required for shadowing. | |
| * | |
| * We highly recommend adding an indirect light as well. | |
| */ | |
| var light: LightNode? = null | |
| set(value) { | |
| field?.let { removeChildNode(it) } | |
| field = value | |
| value?.let { addChildNode(it) } | |
| } | |
| /** | |
| * ### IndirectLight is used to simulate environment lighting | |
| * | |
| * Environment lighting has a two components: | |
| * - irradiance | |
| * - reflections (specular component) | |
| * | |
| * Indirect light are usually captured as high-resolution HDR equirectangular images and | |
| * processed by the cmgen tool to generate the data needed. | |
| * | |
| * You can also process an hdr at runtime but this is more consuming | |
| * | |
| * Currently IndirectLight is intended to be used for "distant probes", that is, to represent | |
| * global illumination from a distant (i.e. at infinity) environment, such as the sky or distant | |
| * mountains. | |
| * Only a single IndirectLight can be used in a Scene. This limitation will be lifted in the | |
| * future. | |
| * | |
| * @see IndirectLight | |
| * @see Scene.setIndirectLight | |
| * @see KTX1Loader.loadIndirectLight | |
| * @see HDRLoader.loadIndirectLight | |
| */ | |
| var indirectLight: IndirectLight? | |
| get() = scene.indirectLight | |
| set(value) { | |
| scene.indirectLight = value | |
| } | |
| /** | |
| * ### The Skybox is drawn last and covers all pixels not touched by geometry | |
| * | |
| * When added to a [SceneView], the `Skybox` fills all untouched pixels. | |
| * | |
| * The Skybox to use to fill untouched pixels, or null to unset the Skybox. | |
| * | |
| * @see Skybox | |
| * @see Scene.setSkybox | |
| */ | |
| var skybox: Skybox? | |
| get() = scene.skybox | |
| set(value) { | |
| scene.skybox = value | |
| } | |
| val childNodes: List<Node> | |
| get() = nodeManager.entities.filter { scene.hasEntity(it) } | |
| .mapNotNull { nodeManager.getNode(it) } | |
| val allChildNodes: List<Node> get() = childNodes + childNodes.flatMap { it.allChildNodes } | |
| /** | |
| * ### Invoked when an frame is processed | |
| * | |
| * Registers a callback to be invoked when a valid Frame is processing. | |
| * | |
| * The callback to be invoked once per frame **immediately before the scene | |
| * is updated**. | |
| * | |
| * The callback will only be invoked if the Frame is considered as valid. | |
| */ | |
| var onFrame: ((frameTime: FrameTime) -> Unit)? = null | |
| var onTap = mutableListOf<(e: MotionEvent, pickingResult: PickingResult) -> Unit>() | |
| // Choreographer is used to schedule new frames | |
| private val choreographer: Choreographer | |
| private val frameScheduler = FrameCallback() | |
| private var swapChain: SwapChain? = null | |
| private val colorGrading: ColorGrading | |
| private val viewAttachmentManager: ViewAttachmentManager | |
| private val selectionModel: Model | |
| private val pickingHandler by lazy { Handler(Looper.getMainLooper()) } | |
| private var currentFrameTime: FrameTime = FrameTime(0) | |
| private var lastTouchEvent: MotionEvent? = null | |
| internal open val isOpaque get() = (background as? ColorDrawable)?.alpha == 255 | |
| init { | |
| displayHelper = DisplayHelper(context) | |
| viewAttachmentManager = ViewAttachmentManager() | |
| choreographer = Choreographer.getInstance() | |
| // Setup Filament | |
| engine = sharedEngine ?: Engine.create() | |
| nodeManager = sharedNodeManager ?: NodeManager(engine) | |
| modelLoader = sharedModelLoader ?: ModelLoader(engine) | |
| iblPrefilter = IBLPrefilter(Filament.engine) | |
| val backgroundColor = (background as? ColorDrawable)?.let { Color(it) } | |
| renderer = engine.createRenderer() | |
| renderer.clearOptions = renderer.clearOptions.apply { | |
| clear = !uiHelper.isOpaque | |
| if (backgroundColor?.a == 1.0f) { | |
| clearColor = backgroundColor.toFloatArray() | |
| } | |
| } | |
| scene = engine.createScene() | |
| view = engine.createView().apply { | |
| // on mobile, better use lower quality color buffer | |
| renderQuality = renderQuality.apply { | |
| hdrColorBuffer = QualityLevel.MEDIUM | |
| } | |
| // dynamic resolution often helps a lot | |
| dynamicResolutionOptions = dynamicResolutionOptions.apply { | |
| enabled = true | |
| quality = QualityLevel.MEDIUM | |
| } | |
| // MSAA is needed with dynamic resolution MEDIUM | |
| multiSampleAntiAliasingOptions = multiSampleAntiAliasingOptions.apply { | |
| enabled = true | |
| } | |
| // FXAA is pretty cheap and helps a lot | |
| antiAliasing = AntiAliasing.FXAA | |
| // ambient occlusion is the cheapest effect that adds a lot of quality | |
| ambientOcclusionOptions = ambientOcclusionOptions.apply { | |
| enabled = true | |
| } | |
| // bloom is pretty expensive but adds a fair amount of realism | |
| bloomOptions = bloomOptions.apply { | |
| enabled = true | |
| } | |
| } | |
| // Change the ToneMapper to FILMIC to avoid some over saturated colors, for example | |
| // material orange 500. | |
| colorGrading = ColorGrading.Builder() | |
| .toneMapping(ColorGrading.ToneMapping.FILMIC) | |
| .build(engine) | |
| view.colorGrading = colorGrading | |
| view.scene = scene | |
| surfaceMirorer = SurfaceMirorer(engine, view, renderer) | |
| cameraNode = camera ?: CameraNode(engine, nodeManager) { | |
| setExposure(kAperture, kShutterSpeed, kSensitivity) | |
| } | |
| this.cameraManipulator = cameraManipulator ?: Manipulator.Builder() | |
| .targetPosition(kDefaultModelPosition) | |
| .viewport(width, height) | |
| .build(Manipulator.Mode.ORBIT) | |
| selectionModel = createModel(context.assets, "models/selection.glb")!! | |
| this.nodesManipulator = nodesManipulator ?: NodesManipulator(engine) { nodeSize -> | |
| createInstance(selectionModel)?.apply { | |
| isSelectable = false | |
| size = size.apply { | |
| xy = nodeSize.xy | |
| } | |
| } | |
| } | |
| setupGestureDetector() | |
| val (r, g, b) = Colors.cct(6_500.0f) | |
| light = LightNode(engine, nodeManager, LightManager.Type.DIRECTIONAL) { | |
| color(r, g, b) | |
| intensity(100_000.0f) | |
| direction(0.0f, -1.0f, 0.0f) | |
| castShadows(true) | |
| } | |
| setupSurfaceView(backgroundColor) | |
| } | |
| open fun onFrame(frameTime: FrameTime) { | |
| if (!uiHelper.isReadyToRender) { | |
| return | |
| } | |
| // Allow the resource loader to finalize textures that have become ready. | |
| modelLoader.onFrame(frameTime) | |
| // Extract the camera basis from the helper and push it to the Filament camera. | |
| cameraManipulator.getLookAt().let { (eye, target, upward) -> | |
| cameraNode?.apply { | |
| worldPosition = eye | |
| lookAt(target, upward) | |
| } | |
| } | |
| // Update child nodes | |
| allChildNodes.forEach { | |
| it.onFrame(frameTime) | |
| } | |
| // Call listeners | |
| onFrame?.invoke(frameTime) | |
| // Render the scene, unless the renderer wants to skip the frame. | |
| if (renderer.beginFrame(swapChain!!, frameTime.nanoseconds)) { | |
| renderer.render(view) | |
| renderer.endFrame() | |
| } | |
| surfaceMirorer.onFrame() | |
| } | |
| fun addChildNode(node: Node) { | |
| (listOf(node) + node.allChildNodes).forEach { childNode -> | |
| childNode.onChildAdded += ::addChildNode | |
| childNode.onChildRemoved += ::removeChildNode | |
| scene.addEntity(childNode.entity) | |
| } | |
| } | |
| fun removeChildNode(node: Node) { | |
| (listOf(node) + node.allChildNodes).forEach { childNode -> | |
| childNode.onChildAdded -= ::addChildNode | |
| childNode.onChildRemoved -= ::removeChildNode | |
| scene.removeEntity(childNode.entity) | |
| } | |
| } | |
| /** | |
| * Loads a [Model] from the contents of a GLB or GLTF file. | |
| * | |
| * @param fileLocation the .glb or .gltf file location: | |
| * - A relative asset file location *models/mymodel.glb* | |
| * - An android resource from the res folder *context.getResourceUri(R.raw.mymodel)* | |
| * - A File path *Uri.fromFile(myModelFile).path* | |
| * - An http or https url *https://mydomain.com/mymodel.glb* | |
| * @param resourceResolver Only used for GLTF file. Return a GLTF resource absolute location | |
| * from a relative file location. The given callback is triggered for each requested resource. | |
| * | |
| * @see FilamentAsset.releaseSourceData | |
| */ | |
| suspend fun loadModel( | |
| fileLocation: String, | |
| resourceResolver: (String) -> String = { ModelLoader.resourceResolver(fileLocation, it) } | |
| ): ModelNode? = modelLoader.loadModel(context, fileLocation, resourceResolver)?.let { | |
| ModelNode(engine, nodeManager, it) | |
| } | |
| /** | |
| * Loads a [Model] from the contents of a GLB or GLTF file within a created coroutine scope. | |
| * | |
| * @see loadModel | |
| */ | |
| fun loadModel( | |
| fileLocation: String, | |
| resourceResolver: (String) -> String = { ModelLoader.resourceResolver(fileLocation, it) }, | |
| onResult: (ModelNode?) -> Unit | |
| ) = modelLoader.loadModel(context, fileLocation, resourceResolver) { model -> | |
| onResult(model?.let { ModelNode(engine, nodeManager, it) }) | |
| } | |
| /** | |
| * Creates a [Model] from the contents of a GLB or GLTF Asset. | |
| * | |
| * @see loadModel | |
| */ | |
| fun createModel(assets: AssetManager, fileLocation: String): Model? = | |
| modelLoader.createModel(assets, fileLocation) | |
| /** | |
| * Loads a primary [Model] with one or more [ModelInstance]s from the contents of a GLB or GLTF | |
| * file. | |
| * | |
| * @param fileLocation the .glb or .gltf file location: | |
| * - A relative asset file location *models/mymodel.glb* | |
| * - An android resource from the res folder *context.getResourceUri(R.raw.mymodel)* | |
| * - A File path *Uri.fromFile(myModelFile).path* | |
| * - An http or https url *https://mydomain.com/mymodel.glb* | |
| * @param count Must be sized to the desired number of instances. If successful, | |
| * this method will return the array with secondary instances whose resources are shared with | |
| * the primary asset. | |
| * @param resourceResolver Only used for GLTF file. Return a GLTF resource absolute location from a | |
| * relative file location. The given callback is triggered for each requested resource. | |
| * | |
| * @see FilamentAsset.releaseSourceData | |
| */ | |
| suspend fun loadInstancedModel( | |
| fileLocation: String, | |
| count: Int, | |
| resourceResolver: (String) -> String = { ModelLoader.resourceResolver(fileLocation, it) } | |
| ): List<ModelNode>? = | |
| modelLoader.loadInstancedModel(context, fileLocation, count, resourceResolver) | |
| ?.let { (model, instances) -> | |
| instances.map { instance -> | |
| ModelNode(engine, nodeManager, instance) | |
| }.also { model.releaseSourceData() } | |
| } | |
| /** | |
| * Loads a primary [Model] with one or more [ModelInstance]s from the contents of a GLB or GLTF | |
| * file with a created coroutine scope. | |
| * | |
| * @see loadInstancedModel | |
| */ | |
| fun loadInstancedModel( | |
| fileLocation: String, | |
| count: Int, | |
| resourceResolver: (String) -> String = { ModelLoader.resourceResolver(fileLocation, it) }, | |
| onResult: (List<ModelNode>?) -> Unit | |
| ) = modelLoader.loadInstancedModel( | |
| context, fileLocation, count, resourceResolver | |
| ) { modelInstance -> | |
| onResult(modelInstance?.let { (model, instances) -> | |
| instances.map { instance -> | |
| ModelNode(engine, nodeManager, instance) | |
| }.also { model.releaseSourceData() } | |
| }) | |
| } | |
| /** | |
| * Adds a new instance to the asset. | |
| * | |
| * Use this with caution. It is more efficient to pre-allocate a max number of instances, and | |
| * gradually add them to the scene as needed. Instances can also be "recycled" by removing and | |
| * re-adding them to the scene. | |
| * | |
| * NOTE: destroyInstance() does not exist because gltfio favors flat arrays for storage of | |
| * entity lists and instance lists, which would be slow to shift. We also wish to discourage | |
| * create/destroy churn, as noted above. | |
| * | |
| * This cannot be called after [FilamentAsset.releaseSourceData]. | |
| * Animation is not supported in new instances. | |
| * @see ModelLoader.createInstancedModel | |
| */ | |
| fun createInstance(model: Model): ModelNode? = | |
| modelLoader.createInstance(model)?.let { ModelNode(engine, nodeManager, it) } | |
| /** | |
| * ### Picks a node at given coordinates | |
| * | |
| * Filament picking works with a small delay, therefore, a callback is used. | |
| * If no node is picked, the callback is invoked with a `null` value instead of a node. | |
| * | |
| * @param x The x coordinate within the `SceneView`. | |
| * @param y The y coordinate within the `SceneView`. | |
| * @param onPickingCompleted Called when picking completes. | |
| */ | |
| fun pickNode(x: Int, y: Int, onResult: (pickingResult: PickingResult) -> Unit) { | |
| // Invert the y coordinate since its origin is at the bottom | |
| val invertedY = height - 1 - y | |
| view.pick(x, invertedY, pickingHandler) { pickingResult -> | |
| onResult(PickingResult(nodeManager, view, camera, pickingResult)) | |
| } | |
| } | |
| fun pickNode(e: MotionEvent, onResult: (pickingResult: PickingResult) -> Unit) = | |
| pickNode(e.x.toInt(), e.y.toInt(), onResult) | |
| fun startMirroring(mediaRecorder: MediaRecorder) = | |
| surfaceMirorer.startMirroring(mediaRecorder.surface, width = width, height = height) | |
| fun stopMirroring(mediaRecorder: MediaRecorder) = | |
| surfaceMirorer.stopMirroring(mediaRecorder.surface) | |
| fun setLifecycle(lifecycle: Lifecycle) { | |
| lifecycle.observe(onResume = { resume() }, onPause = { pause() }, onDestroy = { destroy() }) | |
| } | |
| fun resume() { | |
| viewAttachmentManager.onResume() | |
| choreographer.postFrameCallback(frameScheduler) | |
| } | |
| fun pause() { | |
| choreographer.removeFrameCallback(frameScheduler) | |
| viewAttachmentManager.onPause() | |
| } | |
| fun destroy() { | |
| // Stop any pending frame | |
| choreographer.removeFrameCallback(frameScheduler) | |
| // Always detach the surface before destroying the engine | |
| uiHelper.detach() | |
| modelLoader.destroy() | |
| iblPrefilter.destroy() | |
| engine.destroyRenderer(renderer) | |
| engine.destroyView(view) | |
| engine.destroyColorGrading(colorGrading) | |
| engine.destroyScene(scene) | |
| cameraNode?.let { | |
| engine.destroyCameraComponent(it.entity) | |
| EntityManager.get().destroy(it.entity) | |
| } | |
| // Use runCatching because they should normally already been destroyed by the lifecycle and | |
| // Filament will throw an Exception when destroying them twice. | |
| light?.let { | |
| engine.destroyEntity(it.entity) | |
| EntityManager.get().destroy(it.entity) | |
| } | |
| indirectLight?.let { engine.destroyIndirectLight(it) } | |
| skybox?.let { engine.destroySkybox(it) } | |
| if (nodeManager != sharedNodeManager) { | |
| nodeManager.destroy() | |
| } | |
| if (engine != sharedEngine) { | |
| engine.destroy() | |
| } | |
| Filament.release() | |
| } | |
| @SuppressLint("ClickableViewAccessibility") | |
| override fun onTouchEvent(event: MotionEvent) = super.onTouchEvent(event).also { | |
| lastTouchEvent = event | |
| gestureDetector.onTouchEvent(event) | |
| } | |
| protected fun getActivity(): ComponentActivity = try { | |
| findFragment<Fragment>().requireActivity() | |
| } catch (e: Exception) { | |
| context.getActivity()!! | |
| } | |
| private fun setupGestureDetector() { | |
| gestureDetector = GestureDetector(this, cameraManipulator, nodesManipulator) | |
| gestureDetector.onSingleTapConfirmedListeners += { e, pickingResult -> | |
| onTap.forEach { it(e, pickingResult) } | |
| } | |
| } | |
| private fun setupSurfaceView(backgroundColor: Color?) { | |
| // Setup SurfaceView | |
| uiHelper.renderCallback = SurfaceCallback() | |
| // Must be called before attachTo | |
| uiHelper.isOpaque = isOpaque || backgroundColor?.a == 1.0f | |
| uiHelper.attachTo(this) | |
| } | |
| private fun updateCameraProjection() { | |
| val width = view.viewport.width | |
| val height = view.viewport.height | |
| val aspect = width.toDouble() / height.toDouble() | |
| camera?.setLensProjection( | |
| cameraFocalLength.toDouble(), | |
| aspect, | |
| kNearPlane, | |
| kFarPlane | |
| ) | |
| } | |
| inner class SurfaceCallback : UiHelper.RendererCallback { | |
| override fun onNativeWindowChanged(surface: Surface) { | |
| swapChain?.let { engine.destroySwapChain(it) } | |
| swapChain = engine.createSwapChain(surface) | |
| displayHelper.attach(renderer, 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() | |
| } | |
| } | |
| inner class FrameCallback : Choreographer.FrameCallback { | |
| private val startTime = System.nanoTime() | |
| /** | |
| * Callback that occurs for each display frame. Updates the scene and reposts itself to be | |
| * called by the choreographer on the next frame. | |
| */ | |
| override fun doFrame(frameTimeNanos: Long) { | |
| // Always post the callback for the next frame. | |
| choreographer.postFrameCallback(this) | |
| currentFrameTime = FrameTime(frameTimeNanos, currentFrameTime.nanoseconds) | |
| onFrame(currentFrameTime) | |
| } | |
| } | |
| /** | |
| * Manages a [FrameLayout] that is attached directly to a [WindowManager] that other views can | |
| * be added and removed from. | |
| * | |
| * To render a [android.view.View], the [android.view.View] must be attached to a | |
| * [WindowManager] so that it can be properly drawn. This class encapsulates a [FrameLayout] | |
| * that is attached to a [WindowManager] that other views can be added to as children. | |
| * This allows us to safely and correctly draw the [android.view.View] associated with | |
| * [ViewRenderable]'s while keeping them isolated from the rest of the activities View | |
| * hierarchy. | |
| * | |
| * Additionally, this manages the lifecycle of the window to help ensure that the window is | |
| * added/removed from the WindowManager at the appropriate times. | |
| */ | |
| inner class ViewAttachmentManager { | |
| private val windowManager = context.getSystemService<WindowManager>() | |
| private val windowLayoutParams = WindowManager.LayoutParams( | |
| WindowManager.LayoutParams.WRAP_CONTENT, | |
| WindowManager.LayoutParams.WRAP_CONTENT, | |
| WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, | |
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | |
| or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | |
| or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | |
| or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, | |
| PixelFormat.TRANSLUCENT | |
| ).apply { title = VIEW_RENDERABLE_WINDOW } | |
| val frameLayout = FrameLayout(context) | |
| private val viewLayoutParams = ViewGroup.LayoutParams( | |
| ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT | |
| ) | |
| fun onResume() { | |
| // An owner View can only be added to the WindowManager after the activity has finished | |
| // resuming. Therefore, we must use post to ensure that the window is only added after | |
| // resume is finished. | |
| post { | |
| if (frameLayout.parent == null && isAttachedToWindow) { | |
| windowManager?.addView(frameLayout, windowLayoutParams) | |
| } | |
| } | |
| } | |
| fun onPause() { | |
| // The ownerView must be removed from the WindowManager before the activity is | |
| // destroyed, or the window will be leaked. Therefore we add/remove the ownerView in | |
| // resume/pause. | |
| if (frameLayout.parent != null) { | |
| windowManager?.removeView(frameLayout) | |
| } | |
| } | |
| /** | |
| * Add a ownerView as a child of the [FrameLayout] that is attached to the [SceneView]. | |
| * | |
| * Used by [RenderViewToExternalTexture] to ensure that the ownerView is drawn with all | |
| * appropriate lifecycle events being called correctly. | |
| */ | |
| fun addView(view: android.view.View) { | |
| if (view.parent != frameLayout) { | |
| frameLayout.addView(view, viewLayoutParams) | |
| } | |
| } | |
| /** | |
| * Remove a ownerView from the [FrameLayout] that is attached to the [WindowManager]. | |
| * | |
| * Used by [RenderViewToExternalTexture] to remove ownerView's that no longer need to be | |
| * drawn. | |
| */ | |
| fun removeView(view: android.view.View) { | |
| if (view.parent == frameLayout) { | |
| frameLayout.removeView(view) | |
| } | |
| } | |
| } | |
| companion object { | |
| // Load the library for the utility layer, which in turn loads gltfio and the Filament core. | |
| init { | |
| Utils.init() | |
| } | |
| private const val VIEW_RENDERABLE_WINDOW = "ViewRenderableWindow" | |
| private val kDefaultModelPosition = Position(0.0f, 0.0f, -4.0f) | |
| } | |
| } |
Those ones are the Filament Model Viewer ones. Do you think they should be different for AR?
I think we need to test these values. It is just important to be aware of.
Actually, it doesn't mean the opposite of isTransparent because with alpha=0.5, isTransparent = false but also isOpaque = false
I haven't thought about that.
Even if I would really like to decouple any class from SceneView (in order to have a maximum multi-platforms parts). I had in mind to add SceneView extensions within ModelLoader.kt. Would it be a good way to go?
Yes, I think it will be great for readability.
https://kotlinlang.org/docs/coding-conventions.html#class-layout
Since the guidelines recommend to put related staff together we can group properties and methods by their purpose. For example, members related to rendering, members related to the scene, etc.
OK
Those ones are the Filament Model Viewer ones. Do you think they should be different for AR?
OK
Actually, it doesn't mean the opposite of
isTransparentbecause withalpha=0.5,isTransparent = falsebut alsoisOpaque = falseThe principle is that nodes are not linked to a
SceneViewbut to aNodeManagerso they can be used in multipleSceneView.So, when a
Nodeis added to aSceneView, we populate theScenewith it and all its children but we also want to know when any child is added to it in the future.Even if I would really like to decouple any class from SceneView (in order to have a maximum multi-platforms parts). I had in mind to add
SceneViewextensions withinModelLoader.kt. Would it be a good way to go?OK
https://kotlinlang.org/docs/coding-conventions.html#class-layout