Skip to content

Commit 2b785cc

Browse files
authored
Enable own implementation of camera manipulator (#571)
* feat: Allow own implementation of camera manipulator via CameraGestureDetector.CameraManipulator * doc: Add comments * fix: error in ARSceneView * fix: Overload resolution ambiguity in SceneView and Scene
1 parent 1e303ad commit 2b785cc

File tree

16 files changed

+558
-55
lines changed

16 files changed

+558
-55
lines changed

.idea/gradle.xml

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

arsceneview/src/main/java/io/github/sceneview/ar/ARSceneView.kt

+22-22
Original file line numberDiff line numberDiff line change
@@ -239,28 +239,28 @@ open class ARSceneView @JvmOverloads constructor(
239239
*/
240240
var onSessionUpdated: ((session: Session, frame: Frame) -> Unit)? = null,
241241
) : SceneView(
242-
context,
243-
attrs,
244-
defStyleAttr,
245-
defStyleRes,
246-
sharedEngine,
247-
sharedModelLoader,
248-
sharedMaterialLoader,
249-
sharedEnvironmentLoader,
250-
sharedScene,
251-
sharedView,
252-
sharedRenderer,
253-
sharedCameraNode,
254-
sharedMainLightNode,
255-
sharedEnvironment,
256-
isOpaque,
257-
sharedCollisionSystem,
258-
null,
259-
viewNodeWindowManager,
260-
onGestureListener,
261-
onTouchEvent,
262-
sharedActivity,
263-
sharedLifecycle
242+
context = context,
243+
attrs = attrs,
244+
defStyleAttr = defStyleAttr,
245+
defStyleRes = defStyleRes,
246+
sharedEngine = sharedEngine,
247+
sharedModelLoader = sharedModelLoader,
248+
sharedMaterialLoader = sharedMaterialLoader,
249+
sharedEnvironmentLoader = sharedEnvironmentLoader,
250+
sharedScene = sharedScene,
251+
sharedView = sharedView,
252+
sharedRenderer = sharedRenderer,
253+
sharedCameraNode = sharedCameraNode,
254+
sharedMainLightNode = sharedMainLightNode,
255+
sharedEnvironment = sharedEnvironment,
256+
isOpaque = isOpaque,
257+
sharedCollisionSystem = sharedCollisionSystem,
258+
cameraManipulator = null,
259+
viewNodeWindowManager = viewNodeWindowManager,
260+
onGestureListener = onGestureListener,
261+
onTouchEvent = onTouchEvent,
262+
sharedActivity = sharedActivity,
263+
sharedLifecycle = sharedLifecycle
264264
) {
265265
open val arCore = ARCore(
266266
onSessionCreated = ::onSessionCreated,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
plugins {
2+
id 'com.android.application'
3+
id 'kotlin-android'
4+
}
5+
6+
android {
7+
namespace 'io.github.sceneview.sample.modelviewer.compose.cameramanipulator'
8+
9+
compileSdk 34
10+
11+
defaultConfig {
12+
applicationId "io.github.sceneview.sample.modelviewer.compose.cameramanipulator"
13+
minSdk 28
14+
targetSdk 34
15+
versionCode 1
16+
versionName "1.0.0"
17+
}
18+
19+
buildTypes {
20+
release {
21+
}
22+
}
23+
compileOptions {
24+
sourceCompatibility JavaVersion.VERSION_17
25+
targetCompatibility JavaVersion.VERSION_17
26+
}
27+
kotlinOptions {
28+
jvmTarget = JavaVersion.VERSION_17
29+
}
30+
buildFeatures {
31+
compose true
32+
}
33+
composeOptions {
34+
kotlinCompilerExtensionVersion '1.5.14'
35+
}
36+
androidResources {
37+
noCompress 'filamat', 'ktx'
38+
}
39+
}
40+
41+
dependencies {
42+
implementation project(":samples:common")
43+
44+
implementation "androidx.compose.ui:ui:1.6.7"
45+
implementation "androidx.compose.foundation:foundation:1.6.7"
46+
implementation 'androidx.activity:activity-compose:1.9.0'
47+
implementation 'androidx.compose.material:material:1.6.7'
48+
implementation "androidx.compose.ui:ui-tooling-preview:1.6.7"
49+
implementation "androidx.navigation:navigation-compose:2.7.7"
50+
debugImplementation "androidx.compose.ui:ui-tooling:1.6.7"
51+
52+
// SceneView
53+
releaseImplementation "io.github.sceneview:sceneview:2.2.1"
54+
debugImplementation project(":sceneview")
55+
}
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<application
5+
android:icon="@mipmap/ic_launcher"
6+
android:label="@string/app_name"
7+
android:theme="@style/Theme.SceneViewSample">
8+
<activity
9+
android:name=".MainActivity"
10+
android:configChanges="orientation|screenSize"
11+
android:exported="true"
12+
android:label="@string/app_name"
13+
android:screenOrientation="locked">
14+
<intent-filter>
15+
<action android:name="android.intent.action.MAIN" />
16+
<category android:name="android.intent.category.LAUNCHER" />
17+
</intent-filter>
18+
</activity>
19+
</application>
20+
21+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package io.github.sceneview.sample.modelviewer.compose.cameramanipulator
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.compose.foundation.Image
7+
import androidx.compose.foundation.background
8+
import androidx.compose.foundation.layout.Box
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.navigationBarsPadding
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.layout.width
13+
import androidx.compose.material3.ExperimentalMaterial3Api
14+
import androidx.compose.material3.MaterialTheme
15+
import androidx.compose.material3.Text
16+
import androidx.compose.material3.TopAppBar
17+
import androidx.compose.material3.TopAppBarDefaults
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.res.painterResource
21+
import androidx.compose.ui.res.stringResource
22+
import androidx.compose.ui.unit.dp
23+
import io.github.sceneview.Scene
24+
import io.github.sceneview.collision.CollisionSystem
25+
import io.github.sceneview.gesture.CameraGestureDetector
26+
import io.github.sceneview.math.Position
27+
import io.github.sceneview.math.toVector3
28+
import io.github.sceneview.node.CameraNode
29+
import io.github.sceneview.node.ModelNode
30+
import io.github.sceneview.rememberCameraManipulator
31+
import io.github.sceneview.rememberCameraNode
32+
import io.github.sceneview.rememberCollisionSystem
33+
import io.github.sceneview.rememberEngine
34+
import io.github.sceneview.rememberEnvironmentLoader
35+
import io.github.sceneview.rememberModelLoader
36+
import io.github.sceneview.rememberNode
37+
import io.github.sceneview.rememberOnGestureListener
38+
import io.github.sceneview.rememberView
39+
import io.github.sceneview.sample.SceneviewTheme
40+
import kotlin.math.sign
41+
42+
class MainActivity : ComponentActivity() {
43+
44+
@OptIn(ExperimentalMaterial3Api::class)
45+
override fun onCreate(savedInstanceState: Bundle?) {
46+
super.onCreate(savedInstanceState)
47+
48+
setContent {
49+
SceneviewTheme {
50+
Box(modifier = Modifier.fillMaxSize()) {
51+
val engine = rememberEngine()
52+
val modelLoader = rememberModelLoader(engine)
53+
val environmentLoader = rememberEnvironmentLoader(engine)
54+
val view = rememberView(engine)
55+
val collisionSystem = rememberCollisionSystem(view)
56+
57+
val centerNode = rememberNode(engine)
58+
59+
val cameraNode = rememberCameraNode(engine) {
60+
position = Position(y = -0.5f, z = 2.0f)
61+
lookAt(centerNode)
62+
centerNode.addChildNode(this)
63+
}
64+
65+
val cameraManipulator = rememberCameraManipulator(
66+
creator = {
67+
AdvancedCameraManipulator(
68+
cameraNode = cameraNode,
69+
collisionSystem = collisionSystem,
70+
orbitHomePosition = cameraNode.worldPosition,
71+
targetPosition = centerNode.worldPosition
72+
)
73+
}
74+
)
75+
76+
Scene(
77+
modifier = Modifier.fillMaxSize(),
78+
engine = engine,
79+
modelLoader = modelLoader,
80+
view = view,
81+
cameraNode = cameraNode,
82+
cameraManipulator = cameraManipulator,
83+
childNodes = listOf(centerNode,
84+
rememberNode {
85+
ModelNode(
86+
modelInstance = modelLoader.createModelInstance(
87+
assetFileLocation = "models/damaged_helmet.glb"
88+
),
89+
scaleToUnits = 0.25f
90+
)
91+
}),
92+
collisionSystem = collisionSystem,
93+
environment = environmentLoader.createHDREnvironment(
94+
assetFileLocation = "environments/sky_2k.hdr"
95+
)!!,
96+
onGestureListener = rememberOnGestureListener(
97+
onDoubleTap = { _, node ->
98+
node?.apply {
99+
scale *= 2.0f
100+
}
101+
}
102+
)
103+
)
104+
Image(
105+
modifier = Modifier
106+
.width(192.dp)
107+
.align(Alignment.BottomEnd)
108+
.navigationBarsPadding()
109+
.padding(16.dp)
110+
.background(
111+
color = MaterialTheme.colorScheme.primaryContainer.copy(
112+
alpha = 0.5f
113+
),
114+
shape = MaterialTheme.shapes.medium
115+
)
116+
.padding(8.dp),
117+
painter = painterResource(id = R.drawable.logo),
118+
contentDescription = "Logo"
119+
)
120+
TopAppBar(
121+
title = {
122+
Text(
123+
text = stringResource(id = R.string.app_name)
124+
)
125+
},
126+
colors = TopAppBarDefaults.topAppBarColors(
127+
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.25f),
128+
titleContentColor = MaterialTheme.colorScheme.onPrimary
129+
130+
)
131+
)
132+
}
133+
}
134+
}
135+
}
136+
}
137+
138+
/**
139+
* Override default camera manipulator to achieve relative zooming based on distance from camera to
140+
* target and separation between two fingers.
141+
*
142+
* Target is calculated as closest node to center of finger's position in direction of camera.
143+
*/
144+
class AdvancedCameraManipulator(
145+
private val cameraNode: CameraNode,
146+
private val collisionSystem: CollisionSystem,
147+
orbitHomePosition: Position? = null,
148+
targetPosition: Position? = null
149+
): CameraGestureDetector.DefaultCameraManipulator(
150+
orbitHomePosition = orbitHomePosition,
151+
targetPosition = targetPosition
152+
) {
153+
private var scrollBeginCameraPosition = Position()
154+
private var scrollBeginDistance: Float? = 0f
155+
private var scrollBeginSeparation = 0f
156+
157+
override fun scrollBegin(x: Int, y: Int, separation: Float) {
158+
val hitResults = collisionSystem.hitTest(x.toFloat(), y.toFloat())
159+
scrollBeginDistance = hitResults.firstOrNull()?.node?.position?.let {
160+
(cameraNode.position - it).toVector3().length()
161+
}
162+
scrollBeginCameraPosition = cameraNode.position
163+
scrollBeginSeparation = separation
164+
}
165+
166+
override fun scrollUpdate(x: Int, y: Int, prevSeparation: Float, currSeparation: Float) {
167+
val beginDistance = scrollBeginDistance
168+
if (beginDistance == null) {
169+
super.scrollUpdate(x, y, prevSeparation, currSeparation)
170+
return
171+
}
172+
173+
val movedVector = (cameraNode.position - scrollBeginCameraPosition).toVector3()
174+
val movedDirection = listOf(
175+
movedVector.x.sign,
176+
movedVector.y.sign,
177+
movedVector.z.sign,
178+
).firstOrNull { it != 0f }?.sign ?: 1f
179+
180+
val ratio = scrollBeginSeparation / currSeparation
181+
val moved = movedVector.length() * movedDirection
182+
val target = (beginDistance * ratio)
183+
val adjust = target - (beginDistance - moved)
184+
185+
manipulator.scroll(x, y, adjust)
186+
}
187+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<resources>
2+
<string name="app_name">Model Viewer Compose Advanced Camera Manipulator</string>
3+
</resources>

0 commit comments

Comments
 (0)