Skip to content

Instantly share code, notes, and snippets.

@ThomasGorisse
Created September 5, 2022 12:30
Show Gist options
  • Select an option

  • Save ThomasGorisse/0f144575e25405feaceac09065384bad to your computer and use it in GitHub Desktop.

Select an option

Save ThomasGorisse/0f144575e25405feaceac09065384bad to your computer and use it in GitHub Desktop.
SceneView 1.0.0
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)
}
}
@ThomasGorisse

Copy link
Copy Markdown
Author
  1. Constants
    Should be put in the companion object and named using the upper case with underscores. If a constant is used only once to assign the default value to a property, we can avoid creating a constant since it is already clear what the default value is. It is also more convenient to see the default value inline without having to navigate to the constant.

OK

  1. Near and far planes
    These values should be selected with care to avoid visual artifacts, for example, z-fighting, because of the reduced depth precision in the commonly viewed distance range. It is important to remember that depth values aren't linear.

Those ones are the Filament Model Viewer ones. Do you think they should be different for AR?

  1. Single or multiple listeners
    I suggest using single listeners if they aren't required inside the library because they are easier to maintain and developers that are using the library can set their own listener with multiple handlers.

OK

  1. Transparency
    I think that isTransparent is more commonly used and understandable.

Actually, it doesn't mean the opposite of isTransparent because with alpha=0.5, isTransparent = false but also isOpaque = false

  1. Adding and removing nodes
    Why the addChildNode and removeChildNode methods are added as listeners?

The principle is that nodes are not linked to a SceneView but to a NodeManager so they can be used in multiple SceneView.
So, when a Node is added to a SceneView, we populate the Scene with it and all its children but we also want to know when any child is added to it in the future.

  1. Loading models
    Probably it is better not to duplicate the ModelLoader methods here to reduce the class size (SceneView is one of the largest classes in the library), however, I'm not sure.

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?

  1. ViewAttachmentManager
    Probably it is better to place it in a separate file to reduce the class size too.

OK

  1. Layout
    We should find the most logical order of properties and methods within the class.

https://kotlinlang.org/docs/coding-conventions.html#class-layout

@grassydragon

grassydragon commented Sep 6, 2022

Copy link
Copy Markdown

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment