filament入门挺难的,主要是因为受干扰的信息太多了,有arCore的干扰,也有scenceform的干扰。这里通过制作3D模型查看器的方式,理清他们之间的关系。
有用的信息来源主要有3个:
1,https://medium.com/@philiprideout/getting-started-with-filament-on-android-d10b16f0ec67
Android上的Filament入门
2,https://github.com/thomasgorisse/sceneform-android-sdk
适用于Android的Sceneform SDK-维护
3,https://github.com/Sergiioh/android-model-viewer
适用于Android的GLB和GLTF模型查看器
整体表现为:1实现了页面查看3D模型,2实现了ar查看3D模型,3提供了将1和2集成到你应用的一种思路。依照他们说的一步步来做,就可以实现这样的效果。
想更换ktx资源文件的可以转到上一篇文章。
//www.greatytc.com/p/bbc83a93d0b1
gltf版本的3D模型文件可以从这里得到
https://github.com/KhronosGroup/glTF-Sample-Models
为了防止页面走丢,这里把他们的内容复制进来。
class gltfActivity : Activity()
companion object {
init {
Utils.init()
}
}
private lateinit var surfaceView: SurfaceView
private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
surfaceView = SurfaceView(this).apply { setContentView(this) }
choreographer = Choreographer.getInstance()
modelViewer = ModelViewer(surfaceView)
surfaceView.setOnTouchListener(modelViewer)
loadGltf("BoomBox")
loadEnvironment("courtyard_8k")
}
private fun loadGltf(name: String) {
val buffer = readAsset("models/BoomBox/${name}.gltf")
modelViewer.loadModelGltf(buffer) { uri -> readAsset("models/BoomBox/$uri") }
modelViewer.transformToUnitCube()
}
private fun readAsset(assetName: String): ByteBuffer {
val input = assets.open(assetName)
val bytes = ByteArray(input.available())
input.read(bytes)
return ByteBuffer.wrap(bytes)
}
private val frameCallback = object : Choreographer.FrameCallback {
private val startTime = System.nanoTime()
override fun doFrame(currentTime: Long) {
val seconds = (currentTime - startTime).toDouble() / 1_000_000_000
choreographer.postFrameCallback(this)
modelViewer.animator?.apply {
if (animationCount > 0) {
applyAnimation(0, seconds.toFloat())
}
updateBoneMatrices()
}
modelViewer.render(currentTime)
}
}
private fun Int.getTransform(): Mat4 {
val tm = modelViewer.engine.transformManager
return Mat4.of(*tm.getTransform(tm.getInstance(this), null))
}
private fun Int.setTransform(mat: Mat4) {
val tm = modelViewer.engine.transformManager
tm.setTransform(tm.getInstance(this), mat.toFloatArray())
}
private fun loadEnvironment(ibl: String) {
// Create the indirect light source and add it to the scene.
var buffer = readAsset("envs/$ibl/${ibl}_ibl.ktx")
KtxLoader.createIndirectLight(modelViewer.engine, buffer).apply {
intensity = 50_000f
modelViewer.scene.indirectLight = this
}
// Create the sky box and add it to the scene.
buffer = readAsset("envs/$ibl/${ibl}_skybox.ktx")
KtxLoader.createSkybox(modelViewer.engine, buffer).apply {
modelViewer.scene.skybox = this
}
}
override fun onResume() {
super.onResume()
choreographer.postFrameCallback(frameCallback)
}
override fun onPause() {
super.onPause()
choreographer.removeFrameCallback(frameCallback)
}
override fun onDestroy() {
super.onDestroy()
choreographer.removeFrameCallback(frameCallback)
}
}
class arActivity : AppCompatActivity() {
private var arFragment: ArFragment? = null
private var renderable: Renderable? = null
private class AnimationInstance internal constructor(
var animator: Animator,
index: Int,
var startTime: Long
) {
var duration: Float
var index: Int
init {
duration = animator.getAnimationDuration(index)
this.index = index
}
}
private val animators: MutableSet<AnimationInstance> = ArraySet()
private val colors = Arrays.asList(
Color(0F, 0F, 0F, 1F),
Color(1F, 0F, 0F, 1F),
Color(0F, 1F, 0F, 1F),
Color(0F, 0F, 1F, 1F),
Color(1F, 1F, 0F, 1F),
Color(0F, 1F, 1F, 1F),
Color(1F, 0F, 1F, 1F),
Color(1F, 1F, 1F, 1F)
)
private var nextColor = 0
// CompletableFuture requires api level 24
// FutureReturnValueIgnored is not valid
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!checkIsSupportedDeviceOrFinish(this)) {
return
}
setContentView(R.layout.activity_gltf)
arFragment = supportFragmentManager.findFragmentById(R.id.ux_fragment) as ArFragment?
val weakActivity = WeakReference(this)
ModelRenderable.builder()
.setSource(
this,
R.raw.xxxx
// /* Uri.parse(
//"https://storage.googleapis.com/ar-answers-in-search-models/static/Tiger/model.glb")*/
)
.setIsFilamentGltf(true)
.build()
.thenAccept { modelRenderable: ModelRenderable? ->
val activity = weakActivity.get()
if (activity != null) {
activity.renderable = modelRenderable
}
}
.exceptionally { throwable: Throwable? ->
val toast =
Toast.makeText(this, "Unable to load Tiger renderable", Toast.LENGTH_LONG)
toast.setGravity(Gravity.CENTER, 0, 0)
toast.show()
null
}
arFragment!!.setOnTapArPlaneListener { hitResult: HitResult, plane: Plane?, motionEvent: MotionEvent? ->
if (renderable == null) {
return@setOnTapArPlaneListener
}
// Create the Anchor.
val anchor = hitResult.createAnchor()
val anchorNode =
AnchorNode(anchor)
anchorNode.setParent(arFragment!!.arSceneView.scene)
// Create the transformable model and add it to the anchor.
val model =
TransformableNode(arFragment!!.transformationSystem)
model.setParent(anchorNode)
model.renderable = renderable
model.select()
val filamentAsset =
model.renderableInstance!!.filamentAsset
if (filamentAsset!!.animator.animationCount > 0) {
animators.add(
AnimationInstance(
filamentAsset.animator,
0,
System.nanoTime()
)
)
}
val color = colors[nextColor]
nextColor++
for (i in 0 until renderable!!.submeshCount) {
val material =
renderable!!.getMaterial(i)
material.setFloat4("baseColorFactor", color)
}
val tigerTitleNode = Node()
tigerTitleNode.setParent(model)
tigerTitleNode.isEnabled = false
tigerTitleNode.localPosition = Vector3(0.0f, 1.0f, 0.0f)
ViewRenderable.builder()
.setView(this, R.layout.tiger_card_view)
.build()
.thenAccept { renderable: ViewRenderable? ->
tigerTitleNode.renderable = renderable
tigerTitleNode.isEnabled = true
}
.exceptionally { throwable: Throwable? ->
throw AssertionError(
"Could not load card view.",
throwable
)
}
}
arFragment!!
.getArSceneView()
.scene
.addOnUpdateListener { frameTime: FrameTime? ->
val time = System.nanoTime()
for (animator: AnimationInstance in animators) {
animator.animator.applyAnimation(
animator.index, ((time - animator.startTime) / TimeUnit.SECONDS.toNanos(
1
).toDouble()).toFloat()
% animator.duration
)
animator.animator.updateBoneMatrices()
}
}
}
companion object {
private val TAG = arActivity::class.java.simpleName
private const val MIN_OPENGL_VERSION = 3.0
fun checkIsSupportedDeviceOrFinish(activity: Activity): Boolean {
val openGlVersionString =
(activity.getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.deviceConfigurationInfo
.glEsVersion
if (openGlVersionString.toDouble() < MIN_OPENGL_VERSION) {
Log.e(TAG, "Sceneform requires OpenGL ES 3.0 later")
Toast.makeText(
activity,
"Sceneform requires OpenGL ES 3.0 or later",
Toast.LENGTH_LONG
)
.show()
activity.finish()
return false
}
return true
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.gorisse.thomas.sceneform:sceneform:1.18.3'
implementation 'com.google.android.filament:filament-android:1.9.4'
implementation 'com.google.android.filament:filament-utils-android:1.9.4'
implementation 'com.google.android.filament:gltfio-android:1.9.4'
}
最后,附上工程目录截图,具体的实现细节,请自行摸索。
这个工程可用,但是实现不是很好,因为同样的3D模型,R.raw和assets/models要各复制一份,我没找到更好的实现方式,不知道你有没有找到呢?找到了,要留言告诉我喔~~