-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Add augmented faces - Add native scene viewer through ARCore - Add web scene viewer through Chrome Custom Tabs and official samples - Extract common AR code into ArActivity
- Loading branch information
1 parent
3cab68c
commit d219439
Showing
48 changed files
with
963 additions
and
293 deletions.
There are no files selected for viewing
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
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
{ | ||
materials: [ | ||
{ | ||
name: 'nose', | ||
parameters: [ | ||
{ | ||
texture: 'nose_fur', | ||
}, | ||
], | ||
source: 'sampledata/fox/fox_face_material.mat', | ||
}, | ||
{ | ||
name: 'earMAT', | ||
parameters: [ | ||
{ | ||
texture: 'ear_fur', | ||
}, | ||
], | ||
source: 'sampledata/fox/fox_face_material.mat', | ||
}, | ||
], | ||
model: { | ||
attributes: [ | ||
'Position', | ||
'TexCoord', | ||
'Orientation', | ||
'BoneIndices', | ||
'BoneWeights', | ||
], | ||
collision: {}, | ||
file: 'sampledata/fox/fox_face.fbx', | ||
name: 'fox_face', | ||
}, | ||
samplers: [ | ||
{ | ||
file: 'sampledata/fox/ear_fur.png', | ||
name: 'ear_fur', | ||
pipeline_name: 'ear_fur.png', | ||
}, | ||
{ | ||
file: 'sampledata/fox/nose_fur.png', | ||
name: 'nose_fur', | ||
pipeline_name: '..\\fox\\nose_fur.png', | ||
}, | ||
], | ||
version: '0.54:2', | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
material { | ||
"name" : "Fox Face", | ||
|
||
"parameters" : [ | ||
{ | ||
"type" : "sampler2d", | ||
"name" : "texture" | ||
} | ||
], | ||
"requires" : [ | ||
"position", | ||
"uv0" | ||
], | ||
"shadingModel" : "unlit", | ||
"blending" : "transparent" | ||
} | ||
fragment { | ||
void material(inout MaterialInputs material) { | ||
prepareMaterial(material); | ||
material.baseColor = texture(materialParams_texture, getUV0()); | ||
material.baseColor.rgb = inverseTonemap(material.baseColor.rgb); | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
package fr.smarquis.ar_toolbox | ||
|
||
import android.Manifest.permission.CAMERA | ||
import android.content.Intent | ||
import android.content.pm.PackageManager.PERMISSION_GRANTED | ||
import android.content.res.Configuration.ORIENTATION_LANDSCAPE | ||
import android.graphics.Color | ||
import android.graphics.drawable.Animatable | ||
import android.media.CamcorderProfile | ||
import android.os.Build.VERSION.SDK_INT | ||
import android.os.Build.VERSION_CODES | ||
import android.os.Bundle | ||
import android.view.Gravity | ||
import android.view.Menu | ||
import android.view.MenuItem | ||
import android.view.View | ||
import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | ||
import android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS | ||
import android.widget.ImageView | ||
import android.widget.Toast | ||
import androidx.annotation.LayoutRes | ||
import androidx.annotation.MenuRes | ||
import androidx.appcompat.app.AppCompatActivity | ||
import androidx.appcompat.view.ContextThemeWrapper | ||
import androidx.appcompat.widget.PopupMenu | ||
import androidx.browser.customtabs.CustomTabsIntent | ||
import androidx.core.app.ActivityCompat | ||
import androidx.core.net.toUri | ||
import androidx.core.view.MenuCompat | ||
import com.google.ar.core.ArCoreApk | ||
import com.google.ar.core.Config | ||
import com.google.ar.core.Session | ||
import com.google.ar.core.exceptions.CameraNotAvailableException | ||
import com.google.ar.core.exceptions.UnavailableException | ||
import com.google.ar.sceneform.ArSceneView | ||
import kotlinx.android.synthetic.main.activity_scene.* | ||
|
||
abstract class ArActivity(@LayoutRes contentLayoutId: Int) : AppCompatActivity(contentLayoutId) { | ||
|
||
private companion object { | ||
const val REQUEST_CAMERA_PERMISSION = 1 | ||
} | ||
|
||
private var sessionInitializationFailed: Boolean = false | ||
|
||
private var installRequested: Boolean = false | ||
|
||
private lateinit var videoRecorder: VideoRecorder | ||
|
||
private val arCoreViewerIntent by lazy { | ||
createArCoreViewerIntent( | ||
model = getString(R.string.scene_viewer_native_data).toUri(), | ||
link = getString(R.string.scene_viewer_native_link), | ||
title = null | ||
) | ||
} | ||
|
||
override fun onPostCreate(savedInstanceState: Bundle?) { | ||
super.onPostCreate(savedInstanceState) | ||
videoRecorder = VideoRecorder(this, arSceneView()) { isRecording -> | ||
if (isRecording) { | ||
Toast.makeText(this, R.string.recording, Toast.LENGTH_LONG).show() | ||
} | ||
|
||
recordingIndicator()?.apply { | ||
visibility = if (isRecording) View.VISIBLE else View.GONE | ||
(drawable as? Animatable)?.apply { if (isRecording) start() else stop() } | ||
} | ||
} | ||
recordingIndicator()?.setOnClickListener { | ||
videoRecorder.stop() | ||
videoRecorder.export() | ||
} | ||
} | ||
|
||
override fun onResume() { | ||
super.onResume() | ||
initArSession() | ||
try { | ||
arSceneView().resume() | ||
} catch (ex: CameraNotAvailableException) { | ||
sessionInitializationFailed = true | ||
} | ||
if (hasCameraPermission() && !installRequested && !sessionInitializationFailed) { | ||
renderNavigationBar() | ||
onArResumed() | ||
} | ||
} | ||
|
||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { | ||
if (requestCode == REQUEST_CAMERA_PERMISSION && !hasCameraPermission()) { | ||
redirectToApplicationSettings() | ||
} | ||
} | ||
|
||
override fun onPause() { | ||
super.onPause() | ||
arSceneView().pause() | ||
} | ||
|
||
override fun onDestroy() { | ||
super.onDestroy() | ||
arSceneView().destroy() | ||
videoRecorder.stop() | ||
} | ||
|
||
abstract fun arSceneView(): ArSceneView | ||
|
||
open fun features() = emptySet<Session.Feature>() | ||
|
||
abstract fun config(session: Session): Config | ||
|
||
abstract fun recordingIndicator(): ImageView? | ||
|
||
open fun onArResumed() {} | ||
|
||
private fun hasCameraPermission() = ActivityCompat.checkSelfPermission(this, CAMERA) == PERMISSION_GRANTED | ||
|
||
private fun requestCameraPermission() { | ||
if (!hasCameraPermission()) { | ||
requestPermissions(arrayOf(CAMERA), REQUEST_CAMERA_PERMISSION) | ||
} | ||
} | ||
|
||
private fun renderNavigationBar() { | ||
if (SDK_INT >= VERSION_CODES.O && resources.configuration.orientation != ORIENTATION_LANDSCAPE) { | ||
window.navigationBarColor = Color.WHITE | ||
window.decorView.systemUiVisibility = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | ||
} | ||
} | ||
|
||
private fun initArSession() { | ||
if (arSceneView().session != null) { | ||
return | ||
} | ||
if (!hasCameraPermission()) { | ||
requestCameraPermission() | ||
return | ||
} | ||
if (sessionInitializationFailed) { | ||
return | ||
} | ||
val sessionException: UnavailableException? | ||
try { | ||
val requestInstall = ArCoreApk.getInstance().requestInstall(this, !installRequested) | ||
if (requestInstall == ArCoreApk.InstallStatus.INSTALL_REQUESTED) { | ||
installRequested = true | ||
return | ||
} | ||
installRequested = false | ||
val session = Session(applicationContext, features()) | ||
session.configure(config(session)) | ||
arSceneView().setupSession(session) | ||
return | ||
} catch (e: UnavailableException) { | ||
sessionException = e | ||
} catch (e: Exception) { | ||
sessionException = UnavailableException().apply { initCause(e) } | ||
} | ||
sessionInitializationFailed = true | ||
Toast.makeText(this, sessionException.message(), Toast.LENGTH_LONG).show() | ||
finish() | ||
} | ||
|
||
fun initPopupMenu(anchor: View, @MenuRes menu: Int = 0, onClick: (MenuItem) -> Boolean = { false }, onUpdate: Menu.() -> Unit = {}): PopupMenu { | ||
val popupMenu = PopupMenu(ContextThemeWrapper(this, R.style.PopupMenu), anchor, Gravity.END) | ||
popupMenu.inflate(R.menu.menu_ar) | ||
menu.takeIf { it != 0 }?.let { popupMenu.inflate(it) } | ||
MenuCompat.setGroupDividerEnabled(popupMenu.menu, true) | ||
popupMenu.setOnMenuItemClickListener { | ||
when (it?.itemId) { | ||
R.id.menu_item_screenshot -> arSceneView.screenshot() | ||
R.id.menu_item_quality_2160p -> videoRecorder.start(CamcorderProfile.get(CamcorderProfile.QUALITY_2160P)) | ||
R.id.menu_item_quality_1080p -> videoRecorder.start(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) | ||
R.id.menu_item_quality_720p -> videoRecorder.start(CamcorderProfile.get(CamcorderProfile.QUALITY_720P)) | ||
R.id.menu_item_quality_480p -> videoRecorder.start(CamcorderProfile.get(CamcorderProfile.QUALITY_480P)) | ||
R.id.menu_item_mode_scene -> { | ||
it.isChecked = true | ||
if (this !is SceneActivity) { | ||
startActivity(Intent(this, SceneActivity::class.java)) | ||
finish() | ||
} | ||
return@setOnMenuItemClickListener false | ||
} | ||
R.id.menu_item_mode_faces -> { | ||
it.isChecked = true | ||
if (this !is FaceActivity) { | ||
startActivity(Intent(this, FaceActivity::class.java)) | ||
finish() | ||
} | ||
return@setOnMenuItemClickListener false | ||
} | ||
R.id.menu_item_mode_native_viewer -> { | ||
it.isChecked = true | ||
arCoreViewerIntent.safeStartActivity(this) | ||
} | ||
R.id.menu_item_mode_web_viewer -> { | ||
it.isChecked = true | ||
CustomTabsIntent.Builder().build().launchUrl(this, getString(R.string.scene_viewer_web).toUri()) | ||
} | ||
else -> return@setOnMenuItemClickListener onClick(it) | ||
} | ||
return@setOnMenuItemClickListener true | ||
} | ||
anchor.setOnClickListener { | ||
popupMenu.menu.apply { | ||
findItem(R.id.menu_item_mode_scene).isChecked = false | ||
findItem(R.id.menu_item_mode_faces).isChecked = false | ||
if (this@ArActivity is SceneActivity) findItem(R.id.menu_item_mode_scene).isChecked = true | ||
if (this@ArActivity is FaceActivity) findItem(R.id.menu_item_mode_faces).isChecked = true | ||
findItem(R.id.menu_item_mode_native_viewer).isEnabled = arCoreViewerIntent.resolveActivity(packageManager) != null | ||
findItem(R.id.menu_item_record).isEnabled = !videoRecorder.isRecording | ||
findItem(R.id.menu_item_quality_2160p).isEnabled = CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P) | ||
findItem(R.id.menu_item_quality_1080p).isEnabled = CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_1080P) | ||
findItem(R.id.menu_item_quality_720p).isEnabled = CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_720P) | ||
findItem(R.id.menu_item_quality_480p).isEnabled = CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_480P) | ||
onUpdate(this) | ||
} | ||
popupMenu.show() | ||
} | ||
return popupMenu | ||
} | ||
|
||
} |
Oops, something went wrong.