diff --git a/app/build.gradle b/app/build.gradle
index a1388f9..9d65452 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -6,7 +6,7 @@ apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
- buildToolsVersion "29.0.2"
+ buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.plweegie.magmolecular"
minSdkVersion 24
@@ -32,21 +32,20 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ api project(":sceneformux")
+
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'androidx.core:core-ktx:1.1.0'
+ implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
- implementation 'com.google.android.material:material:1.0.0'
+ implementation 'com.google.android.material:material:1.1.0'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc03'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
- implementation 'com.google.ar:core:1.14.0'
- implementation 'com.google.ar.sceneform:core:1.14.0'
- implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.14.0'
-
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.1'
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1'
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.6'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.1'
implementation 'org.openscience.cdk:cdk-core:2.3'
@@ -61,5 +60,3 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
-
-apply plugin: 'com.google.ar.sceneform.plugin'
diff --git a/build.gradle b/build.gradle
index c036d69..7763e74 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,16 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.3.61'
+ ext.kotlin_version = '1.3.72'
repositories {
google()
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.3'
+ classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath 'com.google.ar.sceneform:plugin:1.14.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -21,7 +20,6 @@ allprojects {
repositories {
google()
jcenter()
-
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e4ad0f9..241cadd 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Nov 13 21:24:18 GMT 2019
+#Thu Jun 18 18:41:53 BST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/sceneformsrc/.gitignore b/sceneformsrc/.gitignore
new file mode 100644
index 0000000..df0dc1f
--- /dev/null
+++ b/sceneformsrc/.gitignore
@@ -0,0 +1,14 @@
+# Android Studio configuration.
+*.iml
+.idea/
+#
+# # Gradle configuration.
+.gradle/
+build/
+#
+# # User configuration.
+local.properties
+#
+# # OS configurations.
+.DS_Store
+
diff --git a/sceneformsrc/build.gradle b/sceneformsrc/build.gradle
new file mode 100644
index 0000000..4639c90
--- /dev/null
+++ b/sceneformsrc/build.gradle
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 Google LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Top-level build file where you can add configuration options common to
+// all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ mavenLocal()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.0.0'
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ mavenLocal()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/sceneformsrc/gradle.properties b/sceneformsrc/gradle.properties
new file mode 100644
index 0000000..f82e516
--- /dev/null
+++ b/sceneformsrc/gradle.properties
@@ -0,0 +1,25 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
diff --git a/sceneformsrc/gradle/wrapper/gradle-wrapper.jar b/sceneformsrc/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..87b738c
Binary files /dev/null and b/sceneformsrc/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/sceneformsrc/gradle/wrapper/gradle-wrapper.properties b/sceneformsrc/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..94b64fd
--- /dev/null
+++ b/sceneformsrc/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 05 19:39:11 CEST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/sceneformsrc/gradlew b/sceneformsrc/gradlew
new file mode 100755
index 0000000..af6708f
--- /dev/null
+++ b/sceneformsrc/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/sceneformsrc/gradlew.bat b/sceneformsrc/gradlew.bat
new file mode 100644
index 0000000..6d57edc
--- /dev/null
+++ b/sceneformsrc/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/sceneformsrc/libs/libsceneform_runtime_schemas.jar b/sceneformsrc/libs/libsceneform_runtime_schemas.jar
new file mode 100644
index 0000000..9fa2628
Binary files /dev/null and b/sceneformsrc/libs/libsceneform_runtime_schemas.jar differ
diff --git a/sceneformsrc/sceneform/build.gradle b/sceneformsrc/sceneform/build.gradle
new file mode 100644
index 0000000..b7e2409
--- /dev/null
+++ b/sceneformsrc/sceneform/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 Google LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 29
+ defaultConfig {
+ // Sceneform requires minSdkVersion >= 24.
+ minSdkVersion 24
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ }
+ compileOptions {
+ // Sceneform libraries use language constructs from Java 8.
+ // Add these compile options if targeting minSdkVersion < 26.
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ implementation files("../libs/libsceneform_runtime_schemas.jar")
+
+ api 'com.google.android.filament:filament-android:1.7.0'
+ api 'com.google.android.filament:gltfio-android:1.7.0'
+ api "com.google.ar:core:1.17.0"
+
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+}
diff --git a/sceneformsrc/sceneform/sampleData/sceneform_camera_material.mat b/sceneformsrc/sceneform/sampleData/sceneform_camera_material.mat
new file mode 100755
index 0000000..eb50c14
--- /dev/null
+++ b/sceneformsrc/sceneform/sampleData/sceneform_camera_material.mat
@@ -0,0 +1,25 @@
+material {
+ "name" : "Camera",
+
+ "parameters" : [
+ {
+ "type" : "samplerExternal",
+ "name" : "cameraTexture"
+ }
+ ],
+ "requires" : [
+ "uv0"
+ ],
+ "vertexDomain" : "device",
+ "depthWrite" : false,
+ "shadingModel" : "unlit",
+ "doubleSided" : true
+}
+fragment {
+ void material(inout MaterialInputs material) {
+ prepareMaterial(material);
+
+ vec4 color = texture(materialParams_cameraTexture, getUV0());
+ material.baseColor.rgb = inverseTonemapSRGB(color.rgb);
+ }
+}
diff --git a/sceneformsrc/sceneform/sampleData/sceneform_opaque_colored_material.mat b/sceneformsrc/sceneform/sampleData/sceneform_opaque_colored_material.mat
new file mode 100755
index 0000000..b799eb5
--- /dev/null
+++ b/sceneformsrc/sceneform/sampleData/sceneform_opaque_colored_material.mat
@@ -0,0 +1,37 @@
+material {
+ "name" : "Opaque Colored",
+
+ "parameters" : [
+ {
+ "type" : "float3",
+ "name" : "color"
+ },
+ {
+ "type" : "float",
+ "name" : "metallic"
+ },
+ {
+ "type" : "float",
+ "name" : "roughness"
+ },
+ {
+ "type" : "float",
+ "name" : "reflectance"
+ }
+ ],
+ "requires" : [
+ "position",
+ "uv0"
+ ],
+ "shadingModel" : "lit",
+ "blending" : "opaque"
+}
+fragment {
+ void material(inout MaterialInputs material) {
+ prepareMaterial(material);
+ material.baseColor.rgb = materialParams.color;
+ material.metallic = materialParams.metallic;
+ material.roughness = materialParams.roughness;
+ material.reflectance = materialParams.reflectance;
+ }
+}
diff --git a/sceneformsrc/sceneform/sampleData/sceneform_opaque_textured_material.mat b/sceneformsrc/sceneform/sampleData/sceneform_opaque_textured_material.mat
new file mode 100755
index 0000000..be9a088
--- /dev/null
+++ b/sceneformsrc/sceneform/sampleData/sceneform_opaque_textured_material.mat
@@ -0,0 +1,37 @@
+material {
+ "name" : "Opaque Textured",
+
+ "parameters" : [
+ {
+ "type" : "sampler2d",
+ "name" : "texture"
+ },
+ {
+ "type" : "float",
+ "name" : "metallic"
+ },
+ {
+ "type" : "float",
+ "name" : "roughness"
+ },
+ {
+ "type" : "float",
+ "name" : "reflectance"
+ }
+ ],
+ "requires" : [
+ "position",
+ "uv0"
+ ],
+ "shadingModel" : "lit",
+ "blending" : "opaque"
+}
+fragment {
+ void material(inout MaterialInputs material) {
+ prepareMaterial(material);
+ material.baseColor = texture(materialParams_texture, getUV0());
+ material.metallic = materialParams.metallic;
+ material.roughness = materialParams.roughness;
+ material.reflectance = materialParams.reflectance;
+ }
+}
diff --git a/sceneformsrc/sceneform/sampleData/sceneform_plane_material.mat b/sceneformsrc/sceneform/sampleData/sceneform_plane_material.mat
new file mode 100755
index 0000000..66cbccc
--- /dev/null
+++ b/sceneformsrc/sceneform/sampleData/sceneform_plane_material.mat
@@ -0,0 +1,74 @@
+material {
+ name : "AR Core Plane Material",
+
+ "parameters": [
+ {
+ "type": "sampler2d",
+ "name": "texture"
+ },
+ {
+ "type": "float3",
+ "name": "color"
+ },
+ {
+ "type": "float2",
+ "name": "uvScale"
+ },
+ {
+ "type": "float3",
+ "name": "focusPoint"
+ },
+ {
+ "type": "float",
+ "name": "radius"
+ }
+ ],
+ "variables" : [
+ "texCoordsAlpha",
+ "smoothWorldPosition"
+ ],
+ "requires" : [
+ "position"
+ ],
+ shadingModel : unlit,
+ "blending": "transparent"
+}
+
+vertex {
+ void materialVertex(inout MaterialVertexInputs material) {
+ float3 pos = getPosition().xyz;
+
+ // The Y position of the vertex represents the Alpha of the plane at this Vertex.
+ material.texCoordsAlpha.z = pos.y;
+
+ // Zero out the Y vertex and compute the world position.
+ pos.y = 0.0;
+ material.worldPosition = mulMat4x4Float3(getWorldFromModelMatrix(), pos);
+ material.smoothWorldPosition = material.worldPosition;
+
+ // Compute the texture coordinates.
+ // The X axis of the texture corresponds to the local X axis of the plane.
+ // The Y axis of the texture corresponds to the local Z axis of the plane.
+ // Scale the texture coordinates by the scale parameter passed into the material.
+ material.texCoordsAlpha.x = pos.x * materialParams.uvScale.x;
+ material.texCoordsAlpha.y = pos.z * materialParams.uvScale.y;
+ }
+}
+
+fragment {
+ void material(inout MaterialInputs material) {
+ prepareMaterial(material);
+
+ material.baseColor = texture(materialParams_texture, variable_texCoordsAlpha.xy);
+ material.baseColor.rgb *= materialParams.color;
+ float textureAlpha = material.baseColor.a;
+
+ // Create a spotlight effect around the focus point.
+ float distToFocus = distance(variable_smoothWorldPosition.xyz, materialParams.focusPoint);
+ float alpha = smoothstep(materialParams.radius, materialParams.radius * .5f, distToFocus);
+
+ // Transparent blending uses pre-multiplied alpha,
+ // so multiply the entire baseColor by the alpha for this fragment.
+ material.baseColor *= variable_texCoordsAlpha.z * alpha;
+ }
+}
diff --git a/sceneformsrc/sceneform/sampleData/sceneform_plane_shadow_material.mat b/sceneformsrc/sceneform/sampleData/sceneform_plane_shadow_material.mat
new file mode 100755
index 0000000..82a57aa
--- /dev/null
+++ b/sceneformsrc/sceneform/sampleData/sceneform_plane_shadow_material.mat
@@ -0,0 +1,24 @@
+material {
+ name : "AR Core Plane Shadow Material",
+ shadingModel : unlit,
+ blending : transparent,
+ shadowMultiplier : true
+}
+
+vertex {
+ void materialVertex(inout MaterialVertexInputs material) {
+ float3 pos = getPosition().xyz;
+
+ // Shift the verticies upwards so we don't z-fight with the plane material.
+ pos.y = 0.005f;
+ material.worldPosition = mulMat4x4Float3(getWorldFromModelMatrix(), pos);
+ }
+}
+
+fragment {
+ void material(inout MaterialInputs material) {
+ prepareMaterial(material);
+
+ material.baseColor = float4(0.0f, 0.0f, 0.0f, .6f);
+ }
+}
diff --git a/sceneformsrc/sceneform/sampleData/sceneform_transparent_colored_material.mat b/sceneformsrc/sceneform/sampleData/sceneform_transparent_colored_material.mat
new file mode 100755
index 0000000..44aa2b9
--- /dev/null
+++ b/sceneformsrc/sceneform/sampleData/sceneform_transparent_colored_material.mat
@@ -0,0 +1,37 @@
+material {
+ "name" : "Transparent Colored",
+
+ "parameters" : [
+ {
+ "type" : "float4",
+ "name" : "color"
+ },
+ {
+ "type" : "float",
+ "name" : "metallic"
+ },
+ {
+ "type" : "float",
+ "name" : "roughness"
+ },
+ {
+ "type" : "float",
+ "name" : "reflectance"
+ }
+ ],
+ "requires" : [
+ "position",
+ "uv0"
+ ],
+ "shadingModel" : "lit",
+ "blending" : "transparent"
+}
+fragment {
+ void material(inout MaterialInputs material) {
+ prepareMaterial(material);
+ material.baseColor = materialParams.color;
+ material.metallic = materialParams.metallic;
+ material.roughness = materialParams.roughness;
+ material.reflectance = materialParams.reflectance;
+ }
+}
diff --git a/sceneformsrc/sceneform/sampleData/sceneform_transparent_textured_material.mat b/sceneformsrc/sceneform/sampleData/sceneform_transparent_textured_material.mat
new file mode 100755
index 0000000..858c82d
--- /dev/null
+++ b/sceneformsrc/sceneform/sampleData/sceneform_transparent_textured_material.mat
@@ -0,0 +1,37 @@
+material {
+ "name" : "Transparent Textured",
+
+ "parameters" : [
+ {
+ "type" : "sampler2d",
+ "name" : "texture"
+ },
+ {
+ "type" : "float",
+ "name" : "metallic"
+ },
+ {
+ "type" : "float",
+ "name" : "roughness"
+ },
+ {
+ "type" : "float",
+ "name" : "reflectance"
+ }
+ ],
+ "requires" : [
+ "position",
+ "uv0"
+ ],
+ "shadingModel" : "lit",
+ "blending" : "transparent"
+}
+fragment {
+ void material(inout MaterialInputs material) {
+ prepareMaterial(material);
+ material.baseColor = texture(materialParams_texture, getUV0());
+ material.metallic = materialParams.metallic;
+ material.roughness = materialParams.roughness;
+ material.reflectance = materialParams.reflectance;
+ }
+}
diff --git a/sceneformsrc/sceneform/sampleData/sceneform_view_material.mat b/sceneformsrc/sceneform/sampleData/sceneform_view_material.mat
new file mode 100755
index 0000000..eae3d46
--- /dev/null
+++ b/sceneformsrc/sceneform/sampleData/sceneform_view_material.mat
@@ -0,0 +1,43 @@
+material {
+ "name" : "View",
+ "defines" : [
+ "baseColor"
+ ],
+ "parameters" : [
+ {
+ "type" : "samplerExternal",
+ "name" : "viewTexture"
+ },
+ {
+ "type" : "float2",
+ "name" : "offsetUv"
+ }
+ ],
+ "requires" : [
+ "position",
+ "uv0"
+ ],
+ "shadingModel" : "unlit",
+ "blending" : "transparent",
+ "doubleSided" : true
+}
+
+fragment {
+ void material(inout MaterialInputs material) {
+ prepareMaterial(material);
+
+ vec2 uv = getUV0();
+
+ if (!gl_FrontFacing) {
+ uv.x = 1.0 - uv.x;
+ }
+
+ // Set offsetUv if we want to invert around an axis.
+ // In front facing camera, set offsetUv.x to 1 and offsetUv.y to 0.
+ uv.x = uv.x + materialParams.offsetUv.x * (1.0 - 2.0 * uv.x);
+ uv.y = uv.y + materialParams.offsetUv.y * (1.0 - 2.0 * uv.y);
+
+ material.baseColor = texture(materialParams_viewTexture, uv);
+ material.baseColor.rgb = inverseTonemapSRGB(material.baseColor.rgb);
+ }
+}
diff --git a/sceneformsrc/sceneform/sampleData/small_empty_house_2k.exr b/sceneformsrc/sceneform/sampleData/small_empty_house_2k.exr
new file mode 100644
index 0000000..b925963
Binary files /dev/null and b/sceneformsrc/sceneform/sampleData/small_empty_house_2k.exr differ
diff --git a/sceneformsrc/sceneform/src/main/AndroidManifest.xml b/sceneformsrc/sceneform/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c189664
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/AnchorNode.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/AnchorNode.java
new file mode 100644
index 0000000..afc6a79
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/AnchorNode.java
@@ -0,0 +1,213 @@
+package com.google.ar.sceneform;
+
+import android.util.Log;
+import androidx.annotation.Nullable;
+import com.google.ar.core.Anchor;
+import com.google.ar.core.Pose;
+import com.google.ar.core.TrackingState;
+import com.google.ar.sceneform.math.MathHelper;
+import com.google.ar.sceneform.math.Quaternion;
+import com.google.ar.sceneform.math.Vector3;
+import java.util.List;
+
+/**
+ * Node that is automatically positioned in world space based on an ARCore Anchor.
+ *
+ *
When the Anchor isn't tracking, all children of this node are disabled.
+ */
+public class AnchorNode extends Node {
+ private static final String TAG = AnchorNode.class.getSimpleName();
+
+ // The anchor that the node is following.
+ @Nullable private Anchor anchor;
+
+ // Determines if the movement between the node's current position and the anchor position should
+ // be smoothed over time or immediate.
+ private boolean isSmoothed = true;
+
+ private boolean wasTracking;
+
+ private static final float SMOOTH_FACTOR = 12.0f;
+
+ /** Create an AnchorNode with no anchor. */
+ public AnchorNode() {}
+
+ /**
+ * Create an AnchorNode with the specified anchor.
+ *
+ * @param anchor the ARCore anchor that this node will automatically position itself to.
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public AnchorNode(Anchor anchor) {
+ setAnchor(anchor);
+ }
+
+ /**
+ * Set an ARCore anchor and force the position of this node to be updated immediately.
+ *
+ * @param anchor the ARCore anchor that this node will automatically position itself to.
+ */
+ public void setAnchor(@Nullable Anchor anchor) {
+ this.anchor = anchor;
+ if (this.anchor != null) {
+ // Force the anchored position to be updated immediately.
+ updateTrackedPose(0.0f, true);
+ }
+
+ // Make sure children are enabled based on the initial state of the anchor.
+ // This is particularly important for Hosted Anchors, which aren't tracking when created.
+ wasTracking = isTracking();
+ setChildrenEnabled(wasTracking || anchor == null);
+ }
+
+ /** Returns the ARCore anchor if it exists or null otherwise. */
+ @Nullable
+ public Anchor getAnchor() {
+ return anchor;
+ }
+
+ /**
+ * Set true to smooth the transition between the node’s current position and the anchor position.
+ * Set false to apply transformations immediately. Smoothing is true by default.
+ *
+ * @param smoothed Whether the transformations are interpolated.
+ */
+ public void setSmoothed(boolean smoothed) {
+ this.isSmoothed = smoothed;
+ }
+
+ /**
+ * Returns true if the transformations are interpolated or false if they are applied immediately.
+ */
+ public boolean isSmoothed() {
+ return isSmoothed;
+ }
+
+ /** Returns true if the ARCore anchor’s tracking state is TRACKING. */
+ public boolean isTracking() {
+ if (anchor == null || anchor.getTrackingState() != TrackingState.TRACKING) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * AnchorNode overrides this to update the node's position to match the ARCore Anchor's position.
+ *
+ * @param frameTime provides time information for the current frame
+ */
+ @Override
+ public void onUpdate(FrameTime frameTime) {
+ updateTrackedPose(frameTime.getDeltaSeconds(), false);
+ }
+
+ /**
+ * Set the local-space position of this node if it is not anchored. If the node is anchored, this
+ * call does nothing.
+ *
+ * @param position The position to apply.
+ */
+ @Override
+ public void setLocalPosition(Vector3 position) {
+ if (anchor != null) {
+ Log.w(TAG, "Cannot call setLocalPosition on AnchorNode while it is anchored.");
+ return;
+ }
+
+ super.setLocalPosition(position);
+ }
+
+ /**
+ * Set the world-space position of this node if it is not anchored. If the node is anchored, this
+ * call does nothing.
+ *
+ * @param position The position to apply.
+ */
+ @Override
+ public void setWorldPosition(Vector3 position) {
+ if (anchor != null) {
+ Log.w(TAG, "Cannot call setWorldPosition on AnchorNode while it is anchored.");
+ return;
+ }
+
+ super.setWorldPosition(position);
+ }
+
+ /**
+ * Set the local-space rotation of this node if it is not anchored. If the node is anchored, this
+ * call does nothing.
+ *
+ * @param rotation The rotation to apply.
+ */
+ @Override
+ public void setLocalRotation(Quaternion rotation) {
+ if (anchor != null) {
+ Log.w(TAG, "Cannot call setLocalRotation on AnchorNode while it is anchored.");
+ return;
+ }
+
+ super.setLocalRotation(rotation);
+ }
+
+ /**
+ * Set the world-space rotation of this node if it is not anchored. If the node is anchored, this
+ * call does nothing.
+ *
+ * @param rotation The rotation to apply.
+ */
+ @Override
+ public void setWorldRotation(Quaternion rotation) {
+ if (anchor != null) {
+ Log.w(TAG, "Cannot call setWorldRotation on AnchorNode while it is anchored.");
+ return;
+ }
+
+ super.setWorldRotation(rotation);
+ }
+
+ private void updateTrackedPose(float deltaSeconds, boolean forceImmediate) {
+ boolean isTracking = isTracking();
+
+ // Hide the children if the anchor isn't currently tracking.
+ if (isTracking != wasTracking) {
+ // The children should be enabled if there is no anchor, even though we aren't tracking in
+ // that case.
+ setChildrenEnabled(isTracking || anchor == null);
+ }
+
+ // isTracking already checks if the anchor is null, but we need the anchor null check for
+ // static analysis.
+ if (anchor == null || !isTracking) {
+ wasTracking = isTracking;
+ return;
+ }
+
+ Pose pose = anchor.getPose();
+ Vector3 desiredPosition = ArHelpers.extractPositionFromPose(pose);
+ Quaternion desiredRotation = ArHelpers.extractRotationFromPose(pose);
+
+ if (isSmoothed && !forceImmediate) {
+ Vector3 position = getWorldPosition();
+ float lerpFactor = MathHelper.clamp(deltaSeconds * SMOOTH_FACTOR, 0, 1);
+ position.set(Vector3.lerp(position, desiredPosition, lerpFactor));
+ super.setWorldPosition(position);
+
+ Quaternion rotation = Quaternion.slerp(getWorldRotation(), desiredRotation, lerpFactor);
+ super.setWorldRotation(rotation);
+ } else {
+ super.setWorldPosition(desiredPosition);
+ super.setWorldRotation(desiredRotation);
+ }
+
+ wasTracking = isTracking;
+ }
+
+ private void setChildrenEnabled(boolean enabled) {
+ List children = getChildren();
+ for (int i = 0; i < children.size(); i++) {
+ Node child = children.get(i);
+ child.setEnabled(enabled);
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/ArHelpers.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/ArHelpers.java
new file mode 100644
index 0000000..d5319f6
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/ArHelpers.java
@@ -0,0 +1,20 @@
+package com.google.ar.sceneform;
+
+import com.google.ar.core.Pose;
+import com.google.ar.sceneform.math.Quaternion;
+import com.google.ar.sceneform.math.Vector3;
+
+/** Helper class for utility functions for interacting with the ARCore API. */
+class ArHelpers {
+ /** Returns a Sceneform {@link Vector3} representing the position from an ARCore {@link Pose}. */
+ static Vector3 extractPositionFromPose(Pose pose) {
+ return new Vector3(pose.tx(), pose.ty(), pose.tz());
+ }
+
+ /**
+ * Returns a Sceneform {@link Quaternion} representing the rotation from an ARCore {@link Pose}.
+ */
+ static Quaternion extractRotationFromPose(Pose pose) {
+ return new Quaternion(pose.qx(), pose.qy(), pose.qz(), pose.qw());
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/ArSceneView.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/ArSceneView.java
new file mode 100644
index 0000000..96a41af
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/ArSceneView.java
@@ -0,0 +1,770 @@
+package com.google.ar.sceneform;
+
+import android.content.Context;
+import android.media.Image;
+
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowManager;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import com.google.ar.core.Anchor;
+import com.google.ar.core.CameraConfig.FacingDirection;
+import com.google.ar.core.Config;
+import com.google.ar.core.Config.LightEstimationMode;
+import com.google.ar.core.Frame;
+import com.google.ar.core.LightEstimate;
+import com.google.ar.core.Pose;
+import com.google.ar.core.Session;
+import com.google.ar.core.TrackingState;
+
+import com.google.ar.core.exceptions.CameraNotAvailableException;
+import com.google.ar.core.exceptions.FatalException;
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.CameraStream;
+import com.google.ar.sceneform.rendering.Color;
+import com.google.ar.sceneform.rendering.EnvironmentalHdrLightEstimate;
+import com.google.ar.sceneform.rendering.GLHelper;
+
+import com.google.ar.sceneform.rendering.PlaneRenderer;
+
+import com.google.ar.sceneform.rendering.Renderer;
+import com.google.ar.sceneform.rendering.ThreadPools;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.ArCoreVersion;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/** A SurfaceView that integrates with ARCore and renders a scene. */
+@SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
+public class ArSceneView extends SceneView {
+ private static final String TAG = ArSceneView.class.getSimpleName();
+ private static final String REPORTED_ENGINE_TYPE = "Sceneform";
+ private static final String REPORTED_ENGINE_VERSION = "1.7";
+ private static final float DEFAULT_PIXEL_INTENSITY = 1.0f;
+ private static final Color DEFAULT_COLOR_CORRECTION = new Color(1, 1, 1);
+
+ /**
+ * When the camera has moved this distance, we create a new anchor to which we attach the Hdr
+ * Lighting scene.
+ */
+ private static final float RECREATE_LIGHTING_ANCHOR_DISTANCE = 0.5f;
+
+ private int cameraTextureId;
+ @Nullable private Session session;
+ @Nullable private Frame currentFrame;
+ @Nullable private Config cachedConfig;
+ private int minArCoreVersionCode;
+
+ private Display display;
+ private CameraStream cameraStream;
+ private PlaneRenderer planeRenderer;
+
+ private boolean lightEstimationEnabled = true;
+ private boolean isLightDirectionUpdateEnabled = true;
+ @Nullable private Consumer onNextHdrLightingEstimate = null;
+
+ private float lastValidPixelIntensity = DEFAULT_PIXEL_INTENSITY;
+ private final Color lastValidColorCorrection = new Color(DEFAULT_COLOR_CORRECTION);
+ @Nullable private Anchor lastValidEnvironmentalHdrAnchor;
+ @Nullable private float[] lastValidEnvironmentalHdrAmbientSphericalHarmonics;
+ @Nullable private float[] lastValidEnvironmentalHdrMainLightDirection;
+ @Nullable private float[] lastValidEnvironmentalHdrMainLightIntensity;
+
+ private final float[] colorCorrectionPixelIntensity = new float[4];
+
+ // pauseResumeTask is modified on the main thread only. It may be completed on background
+ // threads however.
+ private final SequentialTask pauseResumeTask = new SequentialTask();
+
+ /**
+ * Constructs a ArSceneView object and binds it to an Android Context.
+ *
+ * In order to have rendering work correctly, {@link #setupSession(Session)} must be called.
+ *
+ * @see #ArSceneView(Context, AttributeSet)
+ * @param context the Android Context to use
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public ArSceneView(Context context) {
+ // SceneView will initialize the scene, renderer, and camera.
+ super(context);
+ Renderer renderer = Preconditions.checkNotNull(getRenderer());
+ renderer.enablePerformanceMode();
+ initializeAr();
+ }
+
+ /**
+ * Constructs a ArSceneView object and binds it to an Android Context.
+ *
+ *
In order to have rendering work correctly, {@link #setupSession(Session)} must be called.
+ *
+ * @see #setupSession(Session)
+ * @param context the Android Context to use
+ * @param attrs the Android AttributeSet to associate with
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public ArSceneView(Context context, AttributeSet attrs) {
+ // SceneView will initialize the scene, renderer, and camera.
+ super(context, attrs);
+ Renderer renderer = Preconditions.checkNotNull(getRenderer());
+ renderer.enablePerformanceMode();
+ initializeAr();
+ }
+
+ /**
+ * Setup the view with an AR Session. This method must be called once to supply the ARCore
+ * session. The session is needed for any rendering to occur.
+ *
+ *
The session is expected to be configured with the update mode of LATEST_CAMERA_IMAGE.
+ * Without this configuration, the updating of the ARCore session could block the UI Thread
+ * causing poor UI experience.
+ *
+ * @see #ArSceneView(Context, AttributeSet)
+ * @param session the ARCore session to use for this view
+ */
+ public void setupSession(Session session) {
+ if (this.session != null) {
+ Log.w(TAG, "The session has already been setup, cannot set it up again.");
+ return;
+ }
+ // Enforce api level 24
+ AndroidPreconditions.checkMinAndroidApiLevel();
+
+ this.session = session;
+
+ Renderer renderer = Preconditions.checkNotNull(getRenderer());
+ int width = renderer.getDesiredWidth();
+ int height = renderer.getDesiredHeight();
+ if (width != 0 && height != 0) {
+ session.setDisplayGeometry(display.getRotation(), width, height);
+ }
+
+ // Feature config, therefore facing direction, can only be configured once per session.
+ initializeFacingDirection(session);
+
+ // Session needs access to a texture id for updating the camera stream.
+ // Filament and the Main thread each have their own gl context that share resources for this.
+ session.setCameraTextureName(cameraTextureId);
+ }
+
+
+ private void initializeFacingDirection(Session session) {
+ if (session.getCameraConfig().getFacingDirection() == FacingDirection.FRONT) {
+ Renderer renderer = Preconditions.checkNotNull(getRenderer());
+ renderer.setFrontFaceWindingInverted(true);
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /**
+ * Resumes the rendering thread and ARCore session.
+ *
+ *
This must be called from onResume().
+ *
+ * @throws CameraNotAvailableException if the camera can not be opened
+ */
+ @Override
+ public void resume() throws CameraNotAvailableException {
+ resumeSession();
+ resumeScene();
+ }
+
+ /**
+ * Non blocking call to resume the rendering thread and ARCore session in the background
+ *
+ *
This must be called from onResume().
+ *
+ *
If called while another pause or resume is in progress, the resume will be enqueued and
+ * happen after the current operation completes.
+ *
+ * @return A CompletableFuture completed on the main thread once the resume has completed. The
+ * future will be completed exceptionally if the resume can not be done.
+ */
+ public CompletableFuture resumeAsync(Executor executor) {
+ final WeakReference currentSceneView = new WeakReference<>(this);
+ pauseResumeTask.appendRunnable(
+ () -> {
+ ArSceneView arSceneView = currentSceneView.get();
+ if (arSceneView == null) {
+ return;
+ }
+ try {
+ arSceneView.resumeSession();
+ } catch (CameraNotAvailableException e) {
+ throw new RuntimeException(e);
+ }
+ },
+ executor);
+
+ return pauseResumeTask.appendRunnable(
+ () -> {
+ ArSceneView arSceneView = currentSceneView.get();
+ if (arSceneView == null) {
+ return;
+ }
+ arSceneView.resumeScene();
+ },
+ ThreadPools.getMainExecutor());
+ }
+
+ /** Resumes the session without starting the scene. */
+ private void resumeSession() throws CameraNotAvailableException {
+ Session session = this.session;
+ if (session != null) {
+ reportEngineType();
+ session.resume();
+ }
+ }
+
+ /** Resumes the scene without starting the session */
+ private void resumeScene() {
+ try {
+ super.resume();
+ } catch (CameraNotAvailableException ex) {
+ // This exception should not be possible from here
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ /**
+ * Pauses the rendering thread and ARCore session.
+ *
+ * This must be called from onPause().
+ */
+ @Override
+ public void pause() {
+ pauseScene();
+ pauseSession();
+ }
+
+ /**
+ * Non blocking call to pause the rendering thread and ARCore session.
+ *
+ *
This should be called from onPause().
+ *
+ *
If pauseAsync is called while another pause or resume is in progress, the pause will be
+ * enqueued and happen after the current operation completes.
+ *
+ * @return A {@link CompletableFuture} completed on the main thread on the pause has completed.
+ * The future Will will be completed exceptionally if the resume can not be done.
+ */
+ public CompletableFuture pauseAsync(Executor executor) {
+ final WeakReference currentSceneView = new WeakReference<>(this);
+ pauseResumeTask.appendRunnable(
+ () -> {
+ ArSceneView arSceneView = currentSceneView.get();
+ if (arSceneView == null) {
+ return;
+ }
+ arSceneView.pauseScene();
+ },
+ ThreadPools.getMainExecutor());
+
+ return pauseResumeTask
+ .appendRunnable(
+ () -> {
+ ArSceneView arSceneView = currentSceneView.get();
+ if (arSceneView == null) {
+ return;
+ }
+ arSceneView.pauseSession();
+ },
+ executor)
+ .thenAcceptAsync(
+ // Ensure the final completed future is on the main thread.
+ notUsed -> {},
+ ThreadPools.getMainExecutor());
+ }
+
+ /** Pause the session without touching the scene */
+ private void pauseSession() {
+ if (session != null) {
+ session.pause();
+ }
+ }
+
+ /** Pause the scene without touching the session */
+ private void pauseScene() {
+ super.pause();
+ }
+
+ /** @hide */
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (session != null) {
+ int width = right - left;
+ int height = bottom - top;
+ session.setDisplayGeometry(display.getRotation(), width, height);
+ }
+ }
+
+ /**
+ * Enable Light Estimation based on the camera feed. The color and intensity of the sun's indirect
+ * light will be modulated by values provided by ARCore's light estimation. Lit objects in the
+ * scene will be affected.
+ *
+ * @param enable set to true to enable Light Estimation or false to use the default estimate,
+ * which is a pixel intensity of 1.0 and color correction value of white (1.0, 1.0, 1.0).
+ */
+ public void setLightEstimationEnabled(boolean enable) {
+ lightEstimationEnabled = enable;
+ if (!lightEstimationEnabled) {
+ // Update the light probe with the current best light estimate.
+ getScene().setLightEstimate(DEFAULT_COLOR_CORRECTION, DEFAULT_PIXEL_INTENSITY);
+ lastValidPixelIntensity = DEFAULT_PIXEL_INTENSITY;
+ lastValidColorCorrection.set(DEFAULT_COLOR_CORRECTION);
+ }
+ }
+
+ /** @return returns true if light estimation is enabled. */
+ public boolean isLightEstimationEnabled() {
+ return lightEstimationEnabled;
+ }
+
+ /** Returns the ARCore Session used by this view. */
+ @Nullable
+ public Session getSession() {
+ return session;
+ }
+
+ /**
+ * Returns the most recent ARCore Frame if it is available. The frame is updated at the beginning
+ * of each drawing frame. Callers of this method should not retain a reference to the return
+ * value, since it will be invalid to use the ARCore frame starting with the next frame.
+ */
+ @Nullable
+ @UiThread
+ public Frame getArFrame() {
+ return currentFrame;
+ }
+
+ /** Returns PlaneRenderer, used to control plane visualization. */
+ public PlaneRenderer getPlaneRenderer() {
+ return planeRenderer;
+ }
+
+ /**
+ * Before the render call occurs, update the ARCore session to grab the latest frame and update
+ * listeners.
+ *
+ * @return true if the session updated successfully and a new frame was obtained. Update the scene
+ * before rendering.
+ * @hide
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ @Override
+ protected boolean onBeginFrame(long frameTimeNanos) {
+ // No session, no drawing.
+ Session session = this.session;
+ if (session == null) {
+ return false;
+ }
+
+ if (!pauseResumeTask.isDone()) {
+ return false;
+ }
+
+ ensureUpdateMode();
+
+ // Before doing anything update the Frame from ARCore.
+ boolean updated = true;
+ try {
+ Frame frame = session.update();
+ // No frame, no drawing.
+ if (frame == null) {
+ return false;
+ }
+
+ // Setup Camera Stream if needed.
+ if (!cameraStream.isTextureInitialized()) {
+ cameraStream.initializeTexture(frame);
+ }
+
+ // Recalculate camera Uvs if necessary.
+ if (shouldRecalculateCameraUvs(frame)) {
+ cameraStream.recalculateCameraUvs(frame);
+ }
+
+ if (currentFrame != null && currentFrame.getTimestamp() == frame.getTimestamp()) {
+ updated = false;
+ }
+
+ currentFrame = frame;
+ } catch (CameraNotAvailableException e) {
+ Log.w(TAG, "Exception updating ARCore session", e);
+ return false;
+ }
+
+ // No camera, no drawing.
+ com.google.ar.core.Camera currentArCamera = currentFrame.getCamera();
+ if (currentArCamera == null) {
+ getScene().setUseHdrLightEstimate(false);
+ return false;
+ }
+
+ // If ARCore session has changed, update listeners.
+ if (updated) {
+ // At the start of the frame, update the tracked pose of the camera
+ // to use in any calculations during the frame.
+ getScene().getCamera().updateTrackedPose(currentArCamera);
+
+ Frame frame = currentFrame;
+ if (frame != null) {
+ // Update the light estimate.
+ updateLightEstimate(frame);
+ // Update the plane renderer.
+ planeRenderer.update(frame, getWidth(), getHeight());
+ }
+ }
+
+ return updated;
+ }
+
+ private boolean shouldRecalculateCameraUvs(Frame frame) {
+ return frame.hasDisplayGeometryChanged();
+ }
+
+ /** Get the AR light estimate from the frame and then update the scene. */
+ private void updateLightEstimate(Frame frame) {
+ // Just return if Light Estimation is disabled.
+ if (!lightEstimationEnabled || getSession() == null) {
+ return;
+ }
+
+ // Update the Light Probe with the new light estimate.
+ LightEstimate estimate = frame.getLightEstimate();
+
+ if (isEnvironmentalHdrLightingAvailable()) {
+ if (frame.getCamera().getTrackingState() == TrackingState.TRACKING) {
+ updateHdrLightEstimate(
+ estimate, Preconditions.checkNotNull(getSession()), frame.getCamera());
+ }
+ } else {
+ updateNormalLightEstimate(estimate);
+ }
+ }
+
+ /**
+ * Checks whether the sunlight is being updated every frame based on the Environmental HDR
+ * lighting estimate.
+ *
+ * @return true if the sunlight direction is updated every frame, false otherwise.
+ */
+
+ public boolean isLightDirectionUpdateEnabled() {
+ return isLightDirectionUpdateEnabled;
+ }
+
+ /**
+ * Sets whether the sunlight direction generated from Environmental HDR lighting should be updated
+ * every frame. If false the light direction will be updated a single time and then no longer
+ * change.
+ *
+ * This may be used to turn off shadow direction updates when they are distracting or unwanted.
+ *
+ *
The default state is true, with sunlight direction updated every frame.
+ */
+
+ public void setLightDirectionUpdateEnabled(boolean isLightDirectionUpdateEnabled) {
+ this.isLightDirectionUpdateEnabled = isLightDirectionUpdateEnabled;
+ }
+
+ /**
+ * Returns true if the ARCore camera is configured with
+ * Config.LightEstimationMode.ENVIRONMENTAL_HDR. When Environmental HDR lighting mode is enabled,
+ * the resulting light estimates will be applied to the Sceneform Scene.
+ *
+ * @return true if HDR lighting is enabled in Sceneform because ARCore HDR lighting estimation is
+ * enabled.
+ */
+
+ public boolean isEnvironmentalHdrLightingAvailable() {
+ if (cachedConfig == null) {
+ return false;
+ }
+ return (cachedConfig.getLightEstimationMode() == LightEstimationMode.ENVIRONMENTAL_HDR);
+ }
+
+ /**
+ * Causes a serialized version of the next captured light estimate to be saved to disk.
+ *
+ * @hide
+ */
+
+ public void captureLightingValues(
+ Consumer onNextHdrLightingEstimate) {
+ this.onNextHdrLightingEstimate = onNextHdrLightingEstimate;
+ }
+
+
+ void updateHdrLightEstimate(
+ LightEstimate estimate, Session session, com.google.ar.core.Camera camera) {
+ if (estimate.getState() != LightEstimate.State.VALID) {
+ return;
+ }
+ getScene().setUseHdrLightEstimate(true);
+
+ // Updating the direction shouldn't be skipped if it hasn't ever been acquired yet.
+ if (isLightDirectionUpdateEnabled || lastValidEnvironmentalHdrMainLightDirection == null) {
+ boolean needsNewAnchor = false;
+
+ // If the current anchor for the hdr light direction is not tracking, or we have moved too far
+ // then we need a new anchor on which to base our light direction.
+ if (lastValidEnvironmentalHdrAnchor == null
+ || lastValidEnvironmentalHdrAnchor.getTrackingState() != TrackingState.TRACKING) {
+ needsNewAnchor = true;
+ } else {
+ Pose cameraPose = camera.getPose();
+ Vector3 cameraPosition = new Vector3(cameraPose.tx(), cameraPose.ty(), cameraPose.tz());
+ Pose anchorPose = Preconditions.checkNotNull(lastValidEnvironmentalHdrAnchor).getPose();
+ Vector3 anchorPosition = new Vector3(anchorPose.tx(), anchorPose.ty(), anchorPose.tz());
+ needsNewAnchor =
+ Vector3.subtract(cameraPosition, anchorPosition).length()
+ > RECREATE_LIGHTING_ANCHOR_DISTANCE;
+ }
+
+ // If we need a new anchor we destroy the current anchor and try to create a new one. If the
+ // ARCore session is tracking this will succeed, and if not we will stop updating the
+ // deeplight estimate until we begin tracking again.
+ if (needsNewAnchor) {
+ if (lastValidEnvironmentalHdrAnchor != null) {
+ lastValidEnvironmentalHdrAnchor.detach();
+ lastValidEnvironmentalHdrAnchor = null;
+ }
+ lastValidEnvironmentalHdrMainLightDirection = null;
+ if (camera.getTrackingState() == TrackingState.TRACKING) {
+ try {
+ lastValidEnvironmentalHdrAnchor = session.createAnchor(camera.getPose());
+ } catch (FatalException e) {
+ // Hopefully this exception is not truly fatal.
+ Log.e(TAG, "Error trying to create environmental hdr anchor", e);
+ }
+ }
+ }
+
+ // If we have a valid anchor, we update the anchor-relative local direction based on the
+ // current light estimate.
+ if (lastValidEnvironmentalHdrAnchor != null) {
+ float[] mainLightDirection = estimate.getEnvironmentalHdrMainLightDirection();
+ if (mainLightDirection != null) {
+ Pose anchorPose = Preconditions.checkNotNull(lastValidEnvironmentalHdrAnchor).getPose();
+ lastValidEnvironmentalHdrMainLightDirection =
+ anchorPose.inverse().rotateVector(mainLightDirection);
+ }
+ }
+ }
+
+ float[] sphericalHarmonics = estimate.getEnvironmentalHdrAmbientSphericalHarmonics();
+ if (sphericalHarmonics != null) {
+ lastValidEnvironmentalHdrAmbientSphericalHarmonics = sphericalHarmonics;
+ }
+
+ float[] mainLightIntensity = estimate.getEnvironmentalHdrMainLightIntensity();
+ if (mainLightIntensity != null) {
+ lastValidEnvironmentalHdrMainLightIntensity = mainLightIntensity;
+ }
+
+ if (lastValidEnvironmentalHdrAnchor == null
+ || lastValidEnvironmentalHdrMainLightIntensity == null
+ || lastValidEnvironmentalHdrAmbientSphericalHarmonics == null
+ || lastValidEnvironmentalHdrMainLightDirection == null) {
+ return;
+ }
+
+ float mainLightIntensityScalar =
+ Math.max(
+ 1.0f,
+ Math.max(
+ Math.max(
+ lastValidEnvironmentalHdrMainLightIntensity[0],
+ lastValidEnvironmentalHdrMainLightIntensity[1]),
+ lastValidEnvironmentalHdrMainLightIntensity[2]));
+
+ final Color mainLightColor =
+ new Color(
+ lastValidEnvironmentalHdrMainLightIntensity[0] / mainLightIntensityScalar,
+ lastValidEnvironmentalHdrMainLightIntensity[1] / mainLightIntensityScalar,
+ lastValidEnvironmentalHdrMainLightIntensity[2] / mainLightIntensityScalar);
+
+ Image[] cubeMap = estimate.acquireEnvironmentalHdrCubeMap();
+
+ // We calculate the world-space direction relative to the current position of the tracked
+ // anchor.
+ Pose anchorPose = Preconditions.checkNotNull(lastValidEnvironmentalHdrAnchor).getPose();
+ float[] currentLightDirection =
+ anchorPose.rotateVector(
+ Preconditions.checkNotNull(lastValidEnvironmentalHdrMainLightDirection));
+
+ if (onNextHdrLightingEstimate != null) {
+ EnvironmentalHdrLightEstimate lightEstimate =
+ new EnvironmentalHdrLightEstimate(
+ lastValidEnvironmentalHdrAmbientSphericalHarmonics,
+ currentLightDirection,
+ mainLightColor,
+ mainLightIntensityScalar,
+ cubeMap);
+ onNextHdrLightingEstimate.accept(lightEstimate);
+ onNextHdrLightingEstimate = null;
+ }
+
+ getScene()
+ .setEnvironmentalHdrLightEstimate(
+ lastValidEnvironmentalHdrAmbientSphericalHarmonics,
+ currentLightDirection,
+ mainLightColor,
+ mainLightIntensityScalar,
+ cubeMap);
+ for (Image cubeMapImage : cubeMap) {
+ cubeMapImage.close();
+ }
+ }
+
+ private void updateNormalLightEstimate(LightEstimate estimate) {
+ getScene().setUseHdrLightEstimate(false);
+ // Verify that the estimate is valid
+ float pixelIntensity = lastValidPixelIntensity;
+ // Only update the estimate if it is valid.
+ if (estimate.getState() == LightEstimate.State.VALID) {
+ estimate.getColorCorrection(colorCorrectionPixelIntensity, 0);
+ pixelIntensity = Math.max(colorCorrectionPixelIntensity[3], 0.0f);
+ lastValidColorCorrection.set(
+ colorCorrectionPixelIntensity[0],
+ colorCorrectionPixelIntensity[1],
+ colorCorrectionPixelIntensity[2]);
+ }
+ // Update the light probe with the current best light estimate.
+ getScene().setLightEstimate(lastValidColorCorrection, pixelIntensity);
+ // Update the last valid estimate.
+ lastValidPixelIntensity = pixelIntensity;
+ }
+
+ private void initializeAr() {
+ minArCoreVersionCode = ArCoreVersion.getMinArCoreVersionCode(getContext());
+ display = getContext().getSystemService(WindowManager.class).getDefaultDisplay();
+
+ initializePlaneRenderer();
+ initializeCameraStream();
+ }
+
+ private void initializePlaneRenderer() {
+ Renderer renderer = Preconditions.checkNotNull(getRenderer());
+ planeRenderer = new PlaneRenderer(renderer);
+ }
+
+ private void initializeCameraStream() {
+ cameraTextureId = GLHelper.createCameraTexture();
+ Renderer renderer = Preconditions.checkNotNull(getRenderer());
+ cameraStream = new CameraStream(cameraTextureId, renderer);
+ }
+
+ private void ensureUpdateMode() {
+ if (session == null) {
+ return;
+ }
+
+ // Check the update mode.
+ if (minArCoreVersionCode >= ArCoreVersion.VERSION_CODE_1_3) {
+ if (cachedConfig == null) {
+ cachedConfig = session.getConfig();
+ } else {
+ session.getConfig(cachedConfig);
+ }
+
+ Config.UpdateMode updateMode = cachedConfig.getUpdateMode();
+ if (updateMode != Config.UpdateMode.LATEST_CAMERA_IMAGE) {
+ throw new RuntimeException(
+ "Invalid ARCore UpdateMode "
+ + updateMode
+ + ", Sceneform requires that the ARCore session is configured to the "
+ + "UpdateMode LATEST_CAMERA_IMAGE.");
+ }
+ }
+ }
+
+
+
+
+ private static boolean loadUnifiedJni() {return false;}
+
+
+
+
+ private void reportEngineType() {return ;}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private static native void nativeReportEngineType(
+ Session session, String engineType, String engineVersion);
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Camera.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Camera.java
new file mode 100644
index 0000000..90c39ce
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Camera.java
@@ -0,0 +1,524 @@
+package com.google.ar.sceneform;
+
+import android.view.MotionEvent;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import com.google.ar.core.Pose;
+
+import com.google.ar.sceneform.collision.Ray;
+import com.google.ar.sceneform.math.MathHelper;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Quaternion;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.CameraProvider;
+import com.google.ar.sceneform.rendering.EngineInstance;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Represents a virtual camera, which determines the perspective through which the scene is viewed.
+ *
+ * If the camera is part of an {@link ArSceneView}, then the camera automatically tracks the
+ * camera pose from ARCore. Additionally, the following methods will throw {@link
+ * UnsupportedOperationException} when called:
+ *
+ *
+ * - {@link #setParent(NodeParent)} - Camera's parent cannot be changed, it is always the scene.
+ *
- {@link #setLocalPosition(Vector3)} - Camera's position cannot be changed, it is controlled
+ * by the ARCore camera pose.
+ *
- {@link #setLocalRotation(Quaternion)} - Camera's rotation cannot be changed, it is
+ * controlled by the ARCore camera pose.
+ *
- {@link #setWorldPosition(Vector3)} - Camera's position cannot be changed, it is controlled
+ * by the ARCore camera pose.
+ *
- {@link #setWorldRotation(Quaternion)} - Camera's rotation cannot be changed, it is
+ * controlled by the ARCore camera pose.
+ *
+ *
+ * All other functionality in Node is supported. You can access the position and rotation of the
+ * camera, assign a collision shape to the camera, or add children to the camera. Disabling the
+ * camera turns off rendering.
+ */
+public class Camera extends Node implements CameraProvider {
+ private final Matrix viewMatrix = new Matrix();
+ private final Matrix projectionMatrix = new Matrix();
+
+ private static final float DEFAULT_NEAR_PLANE = 0.01f;
+ private static final float DEFAULT_FAR_PLANE = 30.0f;
+ private static final int FALLBACK_VIEW_WIDTH = 1920;
+ private static final int FALLBACK_VIEW_HEIGHT = 1080;
+
+ // Default vertical field of view for non-ar camera.
+ private static final float DEFAULT_VERTICAL_FOV_DEGREES = 90.0f;
+
+ private float nearPlane = DEFAULT_NEAR_PLANE;
+ private float farPlane = DEFAULT_FAR_PLANE;
+
+ private float verticalFov = DEFAULT_VERTICAL_FOV_DEGREES;
+
+ // isArCamera will be true if the Camera is part of an ArSceneView, false otherwise.
+ private final boolean isArCamera;
+ private boolean areMatricesInitialized;
+
+ /**
+ * Constructor just for testing. When testing the Camera directly it is not part of any View, so
+ * the isArCamera flag must be set explicitly.
+ *
+ * @hide
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ Camera(boolean isArCamera) {
+ this.isArCamera = isArCamera;
+ }
+
+ @SuppressWarnings("initialization")
+ Camera(Scene scene) {
+ super();
+ Preconditions.checkNotNull(scene, "Parameter \"scene\" was null.");
+ super.setParent(scene);
+
+ isArCamera = scene.getView() instanceof ArSceneView;
+ if (!isArCamera) {
+ scene
+ .getView()
+ .addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+ refreshProjectionMatrix());
+ }
+ }
+
+ /** @hide */
+ public void setNearClipPlane(float nearPlane) {
+ this.nearPlane = nearPlane;
+
+ // If this is an ArCamera, the projection matrix gets re-created when updateTrackedPose is
+ // called every frame. Otherwise, update it now.
+ if (!isArCamera) {
+ refreshProjectionMatrix();
+ }
+ }
+
+ @Override
+ public float getNearClipPlane() {
+ return nearPlane;
+ }
+
+ /** @hide */
+ public void setFarClipPlane(float farPlane) {
+ this.farPlane = farPlane;
+
+ // If this is an ArCamera, the projection matrix gets re-created when updateTrackedPose is
+ // called every frame. Otherwise, update it now.
+ if (!isArCamera) {
+ refreshProjectionMatrix();
+ }
+ }
+
+ /**
+ * Sets the vertical field of view for the non-ar camera in degrees. If this is an AR camera, then
+ * the fov comes from ARCore and cannot be set so an exception is thrown. The default is 90
+ * degrees.
+ *
+ * @throws UnsupportedOperationException if this is an AR camera
+ */
+
+ public void setVerticalFovDegrees(float verticalFov) {
+ this.verticalFov = verticalFov;
+
+ if (!isArCamera) {
+ refreshProjectionMatrix();
+ } else {
+ throw new UnsupportedOperationException("Cannot set the field of view for AR cameras.");
+ }
+ }
+
+ /**
+ * Gets the vertical field of view for the camera.
+ *
+ * If this is an AR camera, then it is calculated based on the camera information from ARCore
+ * and can vary between device. It can't be calculated until the first frame after the ARCore
+ * session is resumed, in which case an IllegalStateException is thrown.
+ *
+ *
Otherwise, this will return the value set by {@link #setVerticalFovDegrees(float)}, with a
+ * default of 90 degrees.
+ *
+ * @throws IllegalStateException if called before the first frame after ARCore is resumed
+ */
+
+ public float getVerticalFovDegrees() {
+ if (isArCamera) {
+ if (areMatricesInitialized) {
+ double fovRadians = 2.0 * Math.atan(1.0 / projectionMatrix.data[5]);
+ return (float) Math.toDegrees(fovRadians);
+ } else {
+ throw new IllegalStateException(
+ "Cannot get the field of view for AR cameras until the first frame after ARCore has "
+ + "been resumed.");
+ }
+ } else {
+ return verticalFov;
+ }
+ }
+
+ @Override
+ public float getFarClipPlane() {
+ return farPlane;
+ }
+
+ /** @hide Used internally (b/113516741) */
+ @Override
+ public Matrix getViewMatrix() {
+ return viewMatrix;
+ }
+
+ /** @hide Used internally (b/113516741) and within rendering package */
+ @Override
+ public Matrix getProjectionMatrix() {
+ return projectionMatrix;
+ }
+
+ /**
+ * Updates the pose and projection of the camera to match the tracked pose from ARCore.
+ *
+ * @hide Called internally as part of the integration with ARCore, should not be called directly.
+ */
+ @Override
+ public void updateTrackedPose(com.google.ar.core.Camera camera) {
+ Preconditions.checkNotNull(camera, "Parameter \"camera\" was null.");
+
+ // Update the projection matrix.
+ camera.getProjectionMatrix(projectionMatrix.data, 0, nearPlane, farPlane);
+
+ // Update the view matrix.
+ camera.getViewMatrix(viewMatrix.data, 0);
+
+ // Update the node's transformation properties to match the tracked pose.
+ Pose pose = camera.getDisplayOrientedPose();
+ Vector3 position = ArHelpers.extractPositionFromPose(pose);
+ Quaternion rotation = ArHelpers.extractRotationFromPose(pose);
+ super.setWorldPosition(position);
+ super.setWorldRotation(rotation);
+
+ areMatricesInitialized = true;
+ }
+
+ Ray motionEventToRay(MotionEvent motionEvent) {
+ Preconditions.checkNotNull(motionEvent, "Parameter \"motionEvent\" was null.");
+ int index = motionEvent.getActionIndex();
+ return screenPointToRay(motionEvent.getX(index), motionEvent.getY(index));
+ }
+
+ /**
+ * Calculates a ray in world space going from the near-plane of the camera and going through a
+ * point in screen space. Screen space is in Android device screen coordinates: TopLeft = (0, 0)
+ * BottomRight = (Screen Width, Screen Height) The device coordinate space is unaffected by the
+ * orientation of the device.
+ *
+ * @param x X position in device screen coordinates.
+ * @param y Y position in device screen coordinates.
+ */
+ public Ray screenPointToRay(float x, float y) {
+ Vector3 startPoint = new Vector3();
+ Vector3 endPoint = new Vector3();
+
+ unproject(x, y, 0.0f, startPoint);
+ unproject(x, y, 1.0f, endPoint);
+
+ Vector3 direction = Vector3.subtract(endPoint, startPoint);
+
+ return new Ray(startPoint, direction);
+ }
+
+ /**
+ * Convert a point from world space into screen space.
+ *
+ *
The X value is negative when the point is left of the viewport, between 0 and the width of
+ * the {@link SceneView} when the point is within the viewport, and greater than the width when
+ * the point is to the right of the viewport.
+ *
+ *
The Y value is negative when the point is below the viewport, between 0 and the height of
+ * the {@link SceneView} when the point is within the viewport, and greater than the height when
+ * the point is above the viewport.
+ *
+ *
The Z value is always 0 since the return value is a 2D coordinate.
+ *
+ * @param point the point in world space to convert
+ * @return a new vector that represents the point in screen-space.
+ */
+ public Vector3 worldToScreenPoint(Vector3 point) {
+ Matrix m = new Matrix();
+ Matrix.multiply(projectionMatrix, viewMatrix, m);
+
+ int viewWidth = getViewWidth();
+ int viewHeight = getViewHeight();
+ float x = point.x;
+ float y = point.y;
+ float z = point.z;
+ float w = 1.0f;
+
+ // Multiply the world point.
+ Vector3 screenPoint = new Vector3();
+ screenPoint.x = x * m.data[0] + y * m.data[4] + z * m.data[8] + w * m.data[12];
+ screenPoint.y = x * m.data[1] + y * m.data[5] + z * m.data[9] + w * m.data[13];
+ w = x * m.data[3] + y * m.data[7] + z * m.data[11] + w * m.data[15];
+
+ // To clipping space.
+ screenPoint.x = ((screenPoint.x / w) + 1.0f) * 0.5f;
+ screenPoint.y = ((screenPoint.y / w) + 1.0f) * 0.5f;
+
+ // To screen space.
+ screenPoint.x = screenPoint.x * viewWidth;
+ screenPoint.y = screenPoint.y * viewHeight;
+
+ // Invert Y because screen Y points down and Sceneform Y points up.
+ screenPoint.y = viewHeight - screenPoint.y;
+
+ return screenPoint;
+ }
+
+ /** Unsupported operation. Camera's parent cannot be changed, it is always the scene. */
+ @Override
+ public void setParent(@Nullable NodeParent parent) {
+ throw new UnsupportedOperationException(
+ "Camera's parent cannot be changed, it is always the scene.");
+ }
+
+ /**
+ * Set the position of the camera. The camera always {@link #isTopLevel()}, therefore this behaves
+ * the same as {@link #setWorldPosition(Vector3)}.
+ *
+ *
If the camera is part of an {@link ArSceneView}, then this is an unsupported operation.
+ * Camera's position cannot be changed, it is controlled by the ARCore camera pose.
+ */
+ @Override
+ public void setLocalPosition(Vector3 position) {
+ if (isArCamera) {
+ throw new UnsupportedOperationException(
+ "Camera's position cannot be changed, it is controller by the ARCore camera pose.");
+ } else {
+ super.setLocalPosition(position);
+ Matrix.invert(getWorldModelMatrix(), viewMatrix);
+ }
+ }
+
+ /**
+ * Set the rotation of the camera. The camera always {@link #isTopLevel()}, therefore this behaves
+ * the same as {@link #setWorldRotation(Quaternion)}.
+ *
+ *
If the camera is part of an {@link ArSceneView}, then this is an unsupported operation.
+ * Camera's rotation cannot be changed, it is controlled by the ARCore camera pose.
+ */
+ @Override
+ public void setLocalRotation(Quaternion rotation) {
+ if (isArCamera) {
+ throw new UnsupportedOperationException(
+ "Camera's rotation cannot be changed, it is controller by the ARCore camera pose.");
+ } else {
+ super.setLocalRotation(rotation);
+ Matrix.invert(getWorldModelMatrix(), viewMatrix);
+ }
+ }
+
+ /**
+ * Set the position of the camera. The camera always {@link #isTopLevel()}, therefore this behaves
+ * the same as {@link #setLocalPosition(Vector3)}.
+ *
+ *
If the camera is part of an {@link ArSceneView}, then this is an unsupported operation.
+ * Camera's position cannot be changed, it is controlled by the ARCore camera pose.
+ */
+ @Override
+ public void setWorldPosition(Vector3 position) {
+ if (isArCamera) {
+ throw new UnsupportedOperationException(
+ "Camera's position cannot be changed, it is controller by the ARCore camera pose.");
+ } else {
+ super.setWorldPosition(position);
+ Matrix.invert(getWorldModelMatrix(), viewMatrix);
+ }
+ }
+
+ /**
+ * Set the rotation of the camera. The camera always {@link #isTopLevel()}, therefore this behaves
+ * the same as {@link #setLocalRotation(Quaternion)}.
+ *
+ *
If the camera is part of an {@link ArSceneView}, then this is an unsupported operation.
+ * Camera's rotation cannot be changed, it is controlled by the ARCore camera pose.
+ */
+ @Override
+ public void setWorldRotation(Quaternion rotation) {
+ if (isArCamera) {
+ throw new UnsupportedOperationException(
+ "Camera's rotation cannot be changed, it is controller by the ARCore camera pose.");
+ } else {
+ super.setWorldRotation(rotation);
+ Matrix.invert(getWorldModelMatrix(), viewMatrix);
+ }
+ }
+
+ /** @hide Used to explicitly set the projection matrix for testing. */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public void setProjectionMatrix(Matrix matrix) {
+ projectionMatrix.set(matrix.data);
+ }
+
+ private boolean unproject(float x, float y, float z, final Vector3 dest) {
+ Preconditions.checkNotNull(dest, "Parameter \"dest\" was null.");
+
+ Matrix m = new Matrix();
+ Matrix.multiply(projectionMatrix, viewMatrix, m);
+ Matrix.invert(m, m);
+
+ int viewWidth = getViewWidth();
+ int viewHeight = getViewHeight();
+
+ // Invert Y because screen Y points down and Sceneform Y points up.
+ y = viewHeight - y;
+
+ // Normalize between -1 and 1.
+ x = x / viewWidth * 2.0f - 1.0f;
+ y = y / viewHeight * 2.0f - 1.0f;
+ z = 2.0f * z - 1.0f;
+ float w = 1.0f;
+
+ dest.x = x * m.data[0] + y * m.data[4] + z * m.data[8] + w * m.data[12];
+ dest.y = x * m.data[1] + y * m.data[5] + z * m.data[9] + w * m.data[13];
+ dest.z = x * m.data[2] + y * m.data[6] + z * m.data[10] + w * m.data[14];
+ w = x * m.data[3] + y * m.data[7] + z * m.data[11] + w * m.data[15];
+
+ if (MathHelper.almostEqualRelativeAndAbs(w, 0.0f)) {
+ dest.set(0, 0, 0);
+ return false;
+ }
+
+ w = 1.0f / w;
+ dest.set(dest.scaled(w));
+ return true;
+ }
+
+ private int getViewWidth() {
+ Scene scene = getScene();
+ if (scene == null || EngineInstance.isHeadlessMode()) {
+ return FALLBACK_VIEW_WIDTH;
+ }
+
+ return scene.getView().getWidth();
+ }
+
+ private int getViewHeight() {
+ Scene scene = getScene();
+ if (scene == null || EngineInstance.isHeadlessMode()) {
+ return FALLBACK_VIEW_HEIGHT;
+ }
+
+ return scene.getView().getHeight();
+ }
+
+ // Only used if this camera is not controlled by ARCore.
+ private void refreshProjectionMatrix() {
+ if (isArCamera) {
+ return;
+ }
+
+ int width = getViewWidth();
+ int height = getViewHeight();
+
+ if (width == 0 || height == 0) {
+ return;
+ }
+
+ float aspect = (float) width / (float) height;
+ setPerspective(verticalFov, aspect, nearPlane, farPlane);
+ }
+
+ /**
+ * Set the camera perspective based on the field of view, aspect ratio, near and far planes.
+ * verticalFovInDegrees must be greater than zero and less than 180 degrees. far - near must be
+ * greater than zero. aspect must be greater than zero. near and far must be greater than zero.
+ *
+ * @param verticalFovInDegrees vertical field of view in degrees.
+ * @param aspect aspect ratio of the viewport, which is widthInPixels / heightInPixels.
+ * @param near distance in world units from the camera to the near plane, default is 0.1f
+ * @param far distance in world units from the camera to the far plane, default is 100.0f
+ * @throws IllegalArgumentException if any of the following preconditions are not met:
+ *
+ * - 0 < verticalFovInDegrees < 180
+ *
- aspect > 0
+ *
- near > 0
+ *
- far > near
+ *
+ */
+ private void setPerspective(float verticalFovInDegrees, float aspect, float near, float far) {
+ if (verticalFovInDegrees <= 0.0f || verticalFovInDegrees >= 180.0f) {
+ throw new IllegalArgumentException(
+ "Parameter \"verticalFovInDegrees\" is out of the valid range of (0, 180) degrees.");
+ }
+ if (aspect <= 0.0f) {
+ throw new IllegalArgumentException("Parameter \"aspect\" must be greater than zero.");
+ }
+
+ final double fovInRadians = Math.toRadians((double) verticalFovInDegrees);
+ final float top = (float) Math.tan(fovInRadians * 0.5) * near;
+ final float bottom = -top;
+ final float right = top * aspect;
+ final float left = -right;
+
+ setPerspective(left, right, bottom, top, near, far);
+ }
+
+ /**
+ * Set the camera perspective projection in terms of six clip planes. right - left must be greater
+ * than zero. top - bottom must be greater than zero. far - near must be greater than zero. near
+ * and far must be greater than zero.
+ *
+ * @param left offset in world units from the camera to the left plane, at the near plane.
+ * @param right offset in world units from the camera to the right plane, at the near plane.
+ * @param bottom offset in world units from the camera to the bottom plane, at the near plane.
+ * @param top offset in world units from the camera to the top plane, at the near plane.
+ * @param near distance in world units from the camera to the near plane, default is 0.1f
+ * @param far distance in world units from the camera to the far plane, default is 100.0f
+ * @throws IllegalArgumentException if any of the following preconditions are not met:
+ *
+ * - left != right
+ *
- bottom != top
+ *
- near > 0
+ *
- far > near
+ *
+ */
+ private void setPerspective(
+ float left, float right, float bottom, float top, float near, float far) {
+ float[] data = projectionMatrix.data;
+
+ if (left == right || bottom == top || near <= 0.0f || far <= near) {
+ throw new IllegalArgumentException(
+ "Invalid parameters to setPerspective, valid values: "
+ + " width != height, bottom != top, near > 0.0f, far > near");
+ }
+
+ final float reciprocalWidth = 1.0f / (right - left);
+ final float reciprocalHeight = 1.0f / (top - bottom);
+ final float reciprocalDepthRange = 1.0f / (far - near);
+
+ // Right-handed, column major 4x4 matrix.
+ data[0] = 2.0f * near * reciprocalWidth;
+ data[1] = 0.0f;
+ data[2] = 0.0f;
+ data[3] = 0.0f;
+
+ data[4] = 0.0f;
+ data[5] = 2.0f * near * reciprocalHeight;
+ data[6] = 0.0f;
+ data[7] = 0.0f;
+
+ data[8] = (right + left) * reciprocalWidth;
+ data[9] = (top + bottom) * reciprocalHeight;
+ data[10] = -(far + near) * reciprocalDepthRange;
+ data[11] = -1.0f;
+
+ data[12] = 0.0f;
+ data[13] = 0.0f;
+ data[14] = -2.0f * far * near * reciprocalDepthRange;
+ data[15] = 0.0f;
+
+ nearPlane = near;
+ farPlane = far;
+ areMatricesInitialized = true;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/FrameTime.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/FrameTime.java
new file mode 100644
index 0000000..17a79a5
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/FrameTime.java
@@ -0,0 +1,49 @@
+package com.google.ar.sceneform;
+
+import java.util.concurrent.TimeUnit;
+
+/** Provides time information for the current frame. */
+public class FrameTime {
+ private long lastNanoTime = 0;
+ private long deltaNanoseconds = 0;
+
+ private static final float NANOSECONDS_TO_SECONDS = 1.0f / 1_000_000_000.0f;
+
+ /** Get the time in seconds between this frame and the last frame. */
+ public float getDeltaSeconds() {
+ return deltaNanoseconds * NANOSECONDS_TO_SECONDS;
+ }
+
+ /** Get the time in seconds when this frame started. */
+ public float getStartSeconds() {
+ return lastNanoTime * NANOSECONDS_TO_SECONDS;
+ }
+
+ /**
+ * Get the time between this frame and the last frame.
+ *
+ * @param unit The unit time will be returned in
+ * @return The time between frames
+ */
+ public long getDeltaTime(TimeUnit unit) {
+ return unit.convert(deltaNanoseconds, TimeUnit.NANOSECONDS);
+ }
+
+ /**
+ * Get the time when this frame started.
+ *
+ * @param unit The unit time will be returned in
+ * @return The start time of the frame in nanoseconds
+ */
+ public long getStartTime(TimeUnit unit) {
+ return unit.convert(lastNanoTime, TimeUnit.NANOSECONDS);
+ }
+
+ /** FrameTime is only created internally. Update events provide access to it. */
+ FrameTime() {}
+
+ void update(long frameTimeNanos) {
+ deltaNanoseconds = (lastNanoTime == 0) ? 0 : (frameTimeNanos - lastNanoTime);
+ lastNanoTime = frameTimeNanos;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/HitTestResult.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/HitTestResult.java
new file mode 100644
index 0000000..c03286c
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/HitTestResult.java
@@ -0,0 +1,41 @@
+package com.google.ar.sceneform;
+
+import androidx.annotation.Nullable;
+import com.google.ar.sceneform.collision.RayHit;
+
+/**
+ * Stores the results of calls to Scene.hitTest and Scene.hitTestAll. Contains a node that was hit
+ * by the hit test, and associated information.
+ */
+public class HitTestResult extends RayHit {
+ @Nullable private Node node;
+
+ /** @hide */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public void setNode(@Nullable Node node) {
+ this.node = node;
+ }
+
+ /**
+ * The node that was hit by the hit test. Null when there is no hit.
+ *
+ * @return the hit node
+ */
+ @Nullable
+ public Node getNode() {
+ return node;
+ }
+
+ /** @hide */
+ public void set(HitTestResult other) {
+ super.set(other);
+ setNode(other.node);
+ }
+
+ /** @hide */
+ @Override
+ public void reset() {
+ super.reset();
+ node = null;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Node.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Node.java
new file mode 100644
index 0000000..6bb2a15
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Node.java
@@ -0,0 +1,1588 @@
+package com.google.ar.sceneform;
+
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import androidx.annotation.Nullable;
+
+import com.google.ar.sceneform.collision.Collider;
+import com.google.ar.sceneform.collision.CollisionShape;
+import com.google.ar.sceneform.collision.Ray;
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Quaternion;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.Light;
+import com.google.ar.sceneform.rendering.LightInstance;
+import com.google.ar.sceneform.rendering.ModelRenderable;
+import com.google.ar.sceneform.rendering.Renderable;
+import com.google.ar.sceneform.rendering.RenderableInstance;
+import com.google.ar.sceneform.rendering.Renderer;
+
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.ChangeId;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * A Node represents a transformation within the scene graph's hierarchy. It can contain a
+ * renderable for the rendering engine to render.
+ *
+ * Each node can have an arbitrary number of child nodes and one parent. The parent may be
+ * another node, or the scene.
+ */
+public class Node extends NodeParent implements TransformProvider {
+ /**
+ * Interface definition for a callback to be invoked when a touch event is dispatched to this
+ * node. The callback will be invoked before {@link #onTouchEvent(HitTestResult, MotionEvent)} is
+ * called.
+ */
+ public interface OnTouchListener {
+ /**
+ * Handles when a touch event has been dispatched to a node.
+ *
+ *
On {@link MotionEvent#ACTION_DOWN} events, {@link HitTestResult#getNode()} will always be
+ * this node or one of its children. On other events, the touch may have moved causing the
+ * {@link HitTestResult#getNode()} to change (or possibly be null).
+ *
+ * @param hitTestResult represents the node that was touched and information about where it was
+ * touched
+ * @param motionEvent the MotionEvent object containing full information about the event
+ * @return true if the listener has consumed the event, false otherwise
+ */
+ boolean onTouch(HitTestResult hitTestResult, MotionEvent motionEvent);
+ }
+
+ /** Interface definition for a callback to be invoked when a node is tapped. */
+ public interface OnTapListener {
+ /**
+ * Handles when a node has been tapped.
+ *
+ *
{@link HitTestResult#getNode()} will always be this node or one of its children.
+ *
+ * @param hitTestResult represents the node that was tapped and information about where it was
+ * touched
+ * @param motionEvent the {@link MotionEvent#ACTION_UP} MotionEvent that caused the tap
+ */
+ void onTap(HitTestResult hitTestResult, MotionEvent motionEvent);
+ }
+
+ /** Interface definition for callbacks to be invoked when node lifecycle events occur. */
+ public interface LifecycleListener {
+ /**
+ * Notifies the listener that {@link #onActivate()} was called.
+ *
+ * @param node the node that was activated
+ */
+ void onActivated(Node node);
+
+ /**
+ * Notifies the listener that {@link #onUpdate(FrameTime)} was called.
+ *
+ * @param node the node that was updated
+ * @param frameTime provides time information for the current frame
+ */
+ void onUpdated(Node node, FrameTime frameTime);
+
+ /**
+ * Notifies the listener that {@link #onDeactivate()} was called.
+ *
+ * @param node the node that was deactivated
+ */
+ void onDeactivated(Node node);
+ }
+
+ /**
+ * Interface definition for callbacks to be invoked when the transformation of the node changes.
+ */
+ public interface TransformChangedListener {
+
+ /**
+ * Notifies the listener that the transformation of the {@link Node} has changed. Called right
+ * after {@link #onTransformChange(Node)}.
+ *
+ *
The originating node is the most top-level node in the hierarchy that triggered the node
+ * to change. It will always be either the same node or one of its' parents. i.e. if node A's
+ * position is changed, then that will trigger {@link #onTransformChanged(Node, Node)} to be
+ * called for all of it's descendants with the originatingNode being node A.
+ *
+ * @param node the node that changed
+ * @param originatingNode the node that triggered the transformation to change
+ */
+ void onTransformChanged(Node node, Node originatingNode);
+ }
+
+ /** Used to keep track of data for detecting if a tap gesture has occurred on this node. */
+ private static class TapTrackingData {
+ // The node that was being touched when ACTION_DOWN occurred.
+ final Node downNode;
+
+ // The screen-space position that was being touched when ACTION_DOWN occurred.
+ final Vector3 downPosition;
+
+ TapTrackingData(Node downNode, Vector3 downPosition) {
+ this.downNode = downNode;
+ this.downPosition = new Vector3(downPosition);
+ }
+ }
+
+ private static final float DIRECTION_UP_EPSILON = 0.99f;
+
+ // This is the default from the ViewConfiguration class.
+ private static final int DEFAULT_TOUCH_SLOP = 8;
+
+ private static final String DEFAULT_NAME = "Node";
+
+ private static final int LOCAL_TRANSFORM_DIRTY = 1;
+ private static final int WORLD_TRANSFORM_DIRTY = 1 << 1;
+ private static final int WORLD_INVERSE_TRANSFORM_DIRTY = 1 << 2;
+ private static final int WORLD_POSITION_DIRTY = 1 << 3;
+ private static final int WORLD_ROTATION_DIRTY = 1 << 4;
+ private static final int WORLD_SCALE_DIRTY = 1 << 5;
+
+ private static final int WORLD_DIRTY_FLAGS =
+ WORLD_TRANSFORM_DIRTY
+ | WORLD_INVERSE_TRANSFORM_DIRTY
+ | WORLD_POSITION_DIRTY
+ | WORLD_ROTATION_DIRTY
+ | WORLD_SCALE_DIRTY;
+
+ private static final int LOCAL_DIRTY_FLAGS = LOCAL_TRANSFORM_DIRTY | WORLD_DIRTY_FLAGS;
+
+ // Scene Graph fields.
+ @Nullable private Scene scene;
+ // Stores the parent as a node (if the parent is a node) to avoid casting.
+ @Nullable private Node parentAsNode;
+
+ // the name of the node to identify it in the hierarchy
+ @SuppressWarnings("unused")
+ private String name = DEFAULT_NAME;
+
+ // name hash for comparison
+ private int nameHash = DEFAULT_NAME.hashCode();
+
+ /**
+ * WARNING: Do not assign this property directly unless you know what you are doing. Instead, call
+ * setParent. This field is only exposed in the package to be accessible to the class NodeParent.
+ *
+ *
In addition to setting this field, setParent will also do the following things:
+ *
+ *
+ * - Remove this node from its previous parent's children.
+ *
- Add this node to its new parent's children.
+ *
- Recursively update the node's transformation to reflect the change in parent
+ *
- Recursively update the scene field to match the new parent's scene field.
+ *
+ */
+ // The node's parent could be a Node or the scene.
+ @Nullable NodeParent parent;
+
+ // Local transformation fields.
+ private final Vector3 localPosition = new Vector3();
+ private final Quaternion localRotation = new Quaternion();
+ private final Vector3 localScale = new Vector3();
+ private final Matrix cachedLocalModelMatrix = new Matrix();
+
+ // World transformation fields.
+ private final Vector3 cachedWorldPosition = new Vector3();
+ private final Quaternion cachedWorldRotation = new Quaternion();
+ private final Vector3 cachedWorldScale = new Vector3();
+ private final Matrix cachedWorldModelMatrix = new Matrix();
+ private final Matrix cachedWorldModelMatrixInverse = new Matrix();
+
+ /** Determines when various aspects of the node's transform are dirty and must be recalculated. */
+ private int dirtyTransformFlags = LOCAL_DIRTY_FLAGS;
+
+ // Status fields.
+ private boolean enabled = true;
+ private boolean active = false;
+
+ // Rendering fields.
+ private int renderableId = ChangeId.EMPTY_ID;
+ @Nullable private RenderableInstance renderableInstance;
+ // TODO: Right now, lightInstance can cause leaks because it subscribes to event
+ // listeners on Light that will not be disposed unless setLight(null) is called.
+ @Nullable private LightInstance lightInstance;
+
+ // Collision fields.
+ @Nullable private CollisionShape collisionShape;
+ @Nullable private Collider collider;
+
+ // Listeners.
+ @Nullable private OnTouchListener onTouchListener;
+ @Nullable private OnTapListener onTapListener;
+ private final ArrayList lifecycleListeners = new ArrayList<>();
+ private final ArrayList transformChangedListeners = new ArrayList<>();
+ private boolean allowDispatchTransformChangedListeners = true;
+
+ // Stores data used for detecting when a tap has occurred on this node.
+ @Nullable private TapTrackingData tapTrackingData = null;
+
+ /** Creates a node with no parent. */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Node() {
+ AndroidPreconditions.checkUiThread();
+
+ localScale.set(1, 1, 1);
+ cachedWorldScale.set(localScale);
+ }
+
+ /**
+ * Sets the name of this node. Nodes can be found using their names. Multiple nodes may have the
+ * same name, in which case calling {@link NodeParent#findByName(String)} will return the first
+ * node with the given name.
+ *
+ * @param name The name of the node.
+ */
+ public final void setName(String name) {
+ Preconditions.checkNotNull(name, "Parameter \"name\" was null.");
+
+ this.name = name;
+ nameHash = name.hashCode();
+ }
+
+ /** Returns the name of the node. The default value is "Node". */
+ public final String getName() {
+ return name;
+ }
+
+ /**
+ * Changes the parent node of this node. If set to null, this node will be detached from its
+ * parent. The local position, rotation, and scale of this node will remain the same. Therefore,
+ * the world position, rotation, and scale of this node may be different after the parent changes.
+ *
+ * The parent may be another {@link Node} or a {@link Scene}. If it is a scene, then this
+ * {@link Node} is considered top level. {@link #getParent()} will return null, and {@link
+ * #getScene()} will return the scene.
+ *
+ * @see #getParent()
+ * @see #getScene()
+ * @param parent The new parent that this node will be a child of. If null, this node will be
+ * detached from its parent.
+ */
+ public void setParent(@Nullable NodeParent parent) {
+ AndroidPreconditions.checkUiThread();
+
+ if (parent == this.parent) {
+ return;
+ }
+
+ // Disallow dispatching transformed changed here so we don't
+ // send it multiple times when setParent is called.
+ allowDispatchTransformChangedListeners = false;
+ if (parent != null) {
+ // If this node already has a parent, addChild automatically removes it from its old parent.
+ parent.addChild(this);
+ } else if (this.parent != null) {
+ this.parent.removeChild(this);
+ }
+ allowDispatchTransformChangedListeners = true;
+
+ // Make sure transform changed is dispatched.
+ markTransformChangedRecursively(WORLD_DIRTY_FLAGS, this);
+ }
+
+ /**
+ * Returns the scene that this node is part of, null if it isn't part of any scene. A node is part
+ * of a scene if its highest level ancestor is a {@link Scene}
+ */
+ @Nullable
+ public final Scene getScene() {
+ return scene;
+ }
+
+ /**
+ * Returns the parent of this node. If this {@link Node} has a parent, and that parent is a {@link
+ * Node} or {@link Node} subclass, then this function returns the parent as a {@link Node}.
+ * Returns null if the parent is a {@link Scene}, use {@link #getScene()} to retrieve the parent
+ * instead.
+ *
+ * @return the parent as a {@link Node}, if the parent is a {@link Node}.
+ */
+ @Nullable
+ public final Node getParent() {
+ return parentAsNode;
+ }
+
+ /**
+ * Returns true if this node is top level. A node is considered top level if it has no parent or
+ * if the parent is the scene.
+ *
+ * @return true if the node is top level
+ */
+ public boolean isTopLevel() {
+ return parent == null || parent == scene;
+ }
+
+ /**
+ * Checks whether the given node parent is an ancestor of this node recursively.
+ *
+ * @param ancestor the node parent to check
+ * @return true if the node is an ancestor of this node
+ */
+ public final boolean isDescendantOf(NodeParent ancestor) {
+ Preconditions.checkNotNull(ancestor, "Parameter \"ancestor\" was null.");
+
+ NodeParent currentAncestor = parent;
+
+ // Used to iterate up through the hierarchy because NodeParent is just a container for children
+ // and doesn't have its own parent.
+ Node currentAncestorAsNode = parentAsNode;
+
+ while (currentAncestor != null) {
+ // Make sure to do the equality check against currentAncestor instead of currentAncestorAsNode
+ // so that this works with any NodeParent and not just Node.
+ if (currentAncestor == ancestor) {
+ return true;
+ }
+
+ if (currentAncestorAsNode != null) {
+ currentAncestor = currentAncestorAsNode.parent;
+ currentAncestorAsNode = currentAncestorAsNode.parentAsNode;
+ } else {
+ break;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the enabled state of this node. Note that a Node may be enabled but still inactive if it
+ * isn't part of the scene or if its parent is inactive.
+ *
+ * @see #isActive()
+ * @param enabled the new enabled status of the node
+ */
+ public final void setEnabled(boolean enabled) {
+ AndroidPreconditions.checkUiThread();
+
+ if (this.enabled == enabled) {
+ return;
+ }
+
+ this.enabled = enabled;
+ updateActiveStatusRecursively();
+ }
+
+ /**
+ * Gets the enabled state of this node. Note that a Node may be enabled but still inactive if it
+ * isn't part of the scene or if its parent is inactive.
+ *
+ * @see #isActive()
+ * @return the node's enabled status.
+ */
+ public final boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Returns true if the node is active. A node is considered active if it meets ALL of the
+ * following conditions:
+ *
+ *
+ * - The node is part of a scene.
+ *
- the node's parent is active.
+ *
- The node is enabled.
+ *
+ *
+ * An active Node has the following behavior:
+ *
+ *
+ * - The node's {@link #onUpdate(FrameTime)} function will be called every frame.
+ *
- The node's {@link #getRenderable()} will be rendered.
+ *
- The node's {@link #getCollisionShape()} will be checked in calls to Scene.hitTest.
+ *
- The node's {@link #onTouchEvent(HitTestResult, MotionEvent)} function will be called when
+ * the node is touched.
+ *
+ *
+ * @see #onActivate()
+ * @see #onDeactivate()
+ * @return the node's active status
+ */
+ public final boolean isActive() {
+ return active;
+ }
+
+ /**
+ * Registers a callback to be invoked when a touch event is dispatched to this node. The way that
+ * touch events are propagated mirrors the way touches are propagated to Android Views. This is
+ * only called when the node is active.
+ *
+ * When an ACTION_DOWN event occurs, that represents the start of a gesture. ACTION_UP or
+ * ACTION_CANCEL represents when a gesture ends. When a gesture starts, the following is done:
+ *
+ *
+ * - Dispatch touch events to the node that was touched as detected by {@link
+ * Scene#hitTest(MotionEvent)}.
+ *
- If the node doesn't consume the event, recurse upwards through the node's parents and
+ * dispatch the touch event until one of the node's consumes the event.
+ *
- If no nodes consume the event, the gesture is ignored and subsequent events that are part
+ * of the gesture will not be passed to any nodes.
+ *
- If one of the node's consumes the event, then that node will consume all future touch
+ * events for the gesture.
+ *
+ *
+ * When a touch event is dispatched to a node, the event is first passed to the node's {@link
+ * OnTouchListener}. If the {@link OnTouchListener} doesn't handle the event, it is passed to
+ * {@link #onTouchEvent(HitTestResult, MotionEvent)}.
+ *
+ * @see OnTouchListener
+ */
+ public void setOnTouchListener(@Nullable OnTouchListener onTouchListener) {
+ this.onTouchListener = onTouchListener;
+ }
+
+ /**
+ * Registers a callback to be invoked when this node is tapped. If there is a callback registered,
+ * then touch events will not bubble to this node's parent. If the Node.onTouchEvent is overridden
+ * and super.onTouchEvent is not called, then the tap will not occur.
+ *
+ * @see OnTapListener
+ */
+ public void setOnTapListener(@Nullable OnTapListener onTapListener) {
+ if (onTapListener != this.onTapListener) {
+ tapTrackingData = null;
+ }
+
+ this.onTapListener = onTapListener;
+ }
+
+ /**
+ * Adds a listener that will be called when node lifecycle events occur. The listeners will be
+ * called in the order in which they were added.
+ */
+ public void addLifecycleListener(LifecycleListener lifecycleListener) {
+ if (!lifecycleListeners.contains(lifecycleListener)) {
+ lifecycleListeners.add(lifecycleListener);
+ }
+ }
+
+ /** Removes a listener that will be called when node lifecycle events occur. */
+ public void removeLifecycleListener(LifecycleListener lifecycleListener) {
+ lifecycleListeners.remove(lifecycleListener);
+ }
+
+ /** Adds a listener that will be called when the node's transformation changes. */
+ public void addTransformChangedListener(TransformChangedListener transformChangedListener) {
+ if (!transformChangedListeners.contains(transformChangedListener)) {
+ transformChangedListeners.add(transformChangedListener);
+ }
+ }
+
+ /** Removes a listener that will be called when the node's transformation changes. */
+ public void removeTransformChangedListener(TransformChangedListener transformChangedListener) {
+ transformChangedListeners.remove(transformChangedListener);
+ }
+
+ @Override
+ protected final boolean canAddChild(Node child, StringBuilder failureReason) {
+ if (!super.canAddChild(child, failureReason)) {
+ return false;
+ }
+
+ if (isDescendantOf(child)) {
+ failureReason.append("Cannot add child: A node's parent cannot be one of its descendants.");
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected final void onAddChild(Node child) {
+ super.onAddChild(child);
+ child.parentAsNode = this;
+ child.markTransformChangedRecursively(WORLD_DIRTY_FLAGS, child);
+ child.setSceneRecursively(scene);
+ }
+
+ @Override
+ protected final void onRemoveChild(Node child) {
+ super.onRemoveChild(child);
+ child.parentAsNode = null;
+ child.markTransformChangedRecursively(WORLD_DIRTY_FLAGS, child);
+ child.setSceneRecursively(null);
+ }
+
+ private final void markTransformChangedRecursively(int flagsToMark, Node originatingNode) {
+ boolean needsRecursion = false;
+
+ if ((dirtyTransformFlags & flagsToMark) != flagsToMark) {
+ dirtyTransformFlags |= flagsToMark;
+
+ if ((dirtyTransformFlags & WORLD_TRANSFORM_DIRTY) == WORLD_TRANSFORM_DIRTY
+ && collider != null) {
+ collider.markWorldShapeDirty();
+ }
+
+ needsRecursion = true;
+ }
+
+ if (originatingNode.allowDispatchTransformChangedListeners) {
+ dispatchTransformChanged(originatingNode);
+ needsRecursion = true;
+ }
+
+ if (needsRecursion) {
+ // Uses for instead of foreach to avoid unecessary allocations.
+ List children = getChildren();
+ for (int i = 0; i < children.size(); i++) {
+ Node node = children.get(i);
+ node.markTransformChangedRecursively(flagsToMark, originatingNode);
+ }
+ }
+ }
+
+ /**
+ * Gets a copy of the nodes position relative to its parent (local-space). If {@link
+ * #isTopLevel()} is true, then this is the same as {@link #getWorldPosition()}.
+ *
+ * @see #setLocalPosition(Vector3)
+ * @return a new vector that represents the node's local-space position
+ */
+ public final Vector3 getLocalPosition() {
+ return new Vector3(localPosition);
+ }
+
+ /**
+ * Gets a copy of the nodes rotation relative to its parent (local-space). If {@link
+ * #isTopLevel()} is true, then this is the same as {@link #getWorldRotation()}.
+ *
+ * @see #setLocalRotation(Quaternion)
+ * @return a new quaternion that represents the node's local-space rotation
+ */
+ public final Quaternion getLocalRotation() {
+ return new Quaternion(localRotation);
+ }
+
+ /**
+ * Gets a copy of the nodes scale relative to its parent (local-space). If {@link #isTopLevel()}
+ * is true, then this is the same as {@link #getWorldScale()}.
+ *
+ * @see #setLocalScale(Vector3)
+ * @return a new vector that represents the node's local-space scale
+ */
+ public final Vector3 getLocalScale() {
+ return new Vector3(localScale);
+ }
+
+ /**
+ * Get a copy of the nodes world-space position.
+ *
+ * @see #setWorldPosition(Vector3)
+ * @return a new vector that represents the node's world-space position
+ */
+ public final Vector3 getWorldPosition() {
+ return new Vector3(getWorldPositionInternal());
+ }
+
+ /**
+ * Gets a copy of the nodes world-space rotation.
+ *
+ * @see #setWorldRotation(Quaternion)
+ * @return a new quaternion that represents the node's world-space rotation
+ */
+ public final Quaternion getWorldRotation() {
+ return new Quaternion(getWorldRotationInternal());
+ }
+
+ /**
+ * Gets a copy of the nodes world-space scale. Some precision will be lost if the node is skewed.
+ *
+ * @see #setWorldScale(Vector3)
+ * @return a new vector that represents the node's world-space scale
+ */
+ public final Vector3 getWorldScale() {
+ return new Vector3(getWorldScaleInternal());
+ }
+
+ /**
+ * Sets the position of this node relative to its parent (local-space). If {@link #isTopLevel()}
+ * is true, then this is the same as {@link #setWorldPosition(Vector3)}.
+ *
+ * @see #getLocalPosition()
+ * @param position The position to apply.
+ */
+ public void setLocalPosition(Vector3 position) {
+ Preconditions.checkNotNull(position, "Parameter \"position\" was null.");
+
+ localPosition.set(position);
+ markTransformChangedRecursively(LOCAL_DIRTY_FLAGS, this);
+ }
+
+ /**
+ * Sets the rotation of this node relative to its parent (local-space). If {@link #isTopLevel()}
+ * is true, then this is the same as {@link #setWorldRotation(Quaternion)}.
+ *
+ * @see #getLocalRotation()
+ * @param rotation The rotation to apply.
+ */
+ public void setLocalRotation(Quaternion rotation) {
+ Preconditions.checkNotNull(rotation, "Parameter \"rotation\" was null.");
+
+ localRotation.set(rotation);
+ markTransformChangedRecursively(LOCAL_DIRTY_FLAGS, this);
+ }
+
+ /**
+ * Sets the scale of this node relative to its parent (local-space). If {@link #isTopLevel()} is
+ * true, then this is the same as {@link #setWorldScale(Vector3)}.
+ *
+ * @see #getLocalScale()
+ * @param scale The scale to apply.
+ */
+ public void setLocalScale(Vector3 scale) {
+ Preconditions.checkNotNull(scale, "Parameter \"scale\" was null.");
+
+ localScale.set(scale);
+ markTransformChangedRecursively(LOCAL_DIRTY_FLAGS, this);
+ }
+
+ /**
+ * Sets the world-space position of this node.
+ *
+ * @see #getWorldPosition()
+ * @param position The position to apply.
+ */
+ public void setWorldPosition(Vector3 position) {
+ Preconditions.checkNotNull(position, "Parameter \"position\" was null.");
+
+ if (parentAsNode == null) {
+ localPosition.set(position);
+ } else {
+ localPosition.set(parentAsNode.worldToLocalPoint(position));
+ }
+
+ markTransformChangedRecursively(LOCAL_DIRTY_FLAGS, this);
+
+ // We already know the world position, cache it immediately so we don't
+ // need to decompose it.
+ cachedWorldPosition.set(position);
+ dirtyTransformFlags &= ~WORLD_POSITION_DIRTY;
+ }
+
+ /**
+ * Sets the world-space rotation of this node.
+ *
+ * @see #getWorldRotation()
+ * @param rotation The rotation to apply.
+ */
+ public void setWorldRotation(Quaternion rotation) {
+ Preconditions.checkNotNull(rotation, "Parameter \"rotation\" was null.");
+
+ if (parentAsNode == null) {
+ localRotation.set(rotation);
+ } else {
+ localRotation.set(
+ Quaternion.multiply(parentAsNode.getWorldRotationInternal().inverted(), rotation));
+ }
+
+ markTransformChangedRecursively(LOCAL_DIRTY_FLAGS, this);
+
+ // We already know the world rotation, cache it immediately so we don't
+ // need to decompose it.
+ cachedWorldRotation.set(rotation);
+ dirtyTransformFlags &= ~WORLD_ROTATION_DIRTY;
+ }
+
+ /**
+ * Sets the world-space scale of this node.
+ *
+ * @see #getWorldScale()
+ * @param scale The scale to apply.
+ */
+ public void setWorldScale(Vector3 scale) {
+ Preconditions.checkNotNull(scale, "Parameter \"scale\" was null.");
+
+ if (parentAsNode != null) {
+ Node parentAsNode = this.parentAsNode;
+
+ // Compute local matrix with scale = 1.
+ // Disallow dispatch transform changed here so we don't send the event multiple times
+ // during setWorldScale.
+ allowDispatchTransformChangedListeners = false;
+ setLocalScale(Vector3.one());
+ allowDispatchTransformChangedListeners = true;
+ Matrix localModelMatrix = getLocalModelMatrixInternal();
+
+ Matrix.multiply(
+ parentAsNode.getWorldModelMatrixInternal(), localModelMatrix, cachedWorldModelMatrix);
+
+ // Both matrices get recomputed, so we can use them as temporary storage.
+ Matrix worldS = localModelMatrix;
+ worldS.makeScale(scale);
+
+ Matrix inv = cachedWorldModelMatrix;
+ Matrix.invert(cachedWorldModelMatrix, inv);
+
+ Matrix.multiply(inv, worldS, inv);
+ inv.decomposeScale(localScale);
+ setLocalScale(localScale);
+ } else {
+ setLocalScale(scale);
+ }
+
+ // We already know the world scale, cache it immediately so we don't
+ // need to decompose it.
+ cachedWorldScale.set(scale);
+ dirtyTransformFlags &= ~WORLD_SCALE_DIRTY;
+ }
+
+ /**
+ * Converts a point in the local-space of this node to world-space.
+ *
+ * @param point the point in local-space to convert
+ * @return a new vector that represents the point in world-space
+ */
+ public final Vector3 localToWorldPoint(Vector3 point) {
+ Preconditions.checkNotNull(point, "Parameter \"point\" was null.");
+
+ return getWorldModelMatrixInternal().transformPoint(point);
+ }
+
+ /**
+ * Converts a point in world-space to the local-space of this node.
+ *
+ * @param point the point in world-space to convert
+ * @return a new vector that represents the point in local-space
+ */
+ public final Vector3 worldToLocalPoint(Vector3 point) {
+ Preconditions.checkNotNull(point, "Parameter \"point\" was null.");
+
+ return getWorldModelMatrixInverseInternal().transformPoint(point);
+ }
+
+ /**
+ * Converts a direction from the local-space of this node to world-space. Not impacted by the
+ * position or scale of the node.
+ *
+ * @param direction the direction in local-space to convert
+ * @return a new vector that represents the direction in world-space
+ */
+ public final Vector3 localToWorldDirection(Vector3 direction) {
+ Preconditions.checkNotNull(direction, "Parameter \"direction\" was null.");
+
+ return Quaternion.rotateVector(getWorldRotationInternal(), direction);
+ }
+
+ /**
+ * Converts a direction from world-space to the local-space of this node. Not impacted by the
+ * position or scale of the node.
+ *
+ * @param direction the direction in world-space to convert
+ * @return a new vector that represents the direction in local-space
+ */
+ public final Vector3 worldToLocalDirection(Vector3 direction) {
+ Preconditions.checkNotNull(direction, "Parameter \"direction\" was null.");
+
+ return Quaternion.inverseRotateVector(getWorldRotationInternal(), direction);
+ }
+
+ /**
+ * Gets the world-space forward vector (-z) of this node.
+ *
+ * @return a new vector that represents the node's forward direction in world-space
+ */
+ public final Vector3 getForward() {
+ return localToWorldDirection(Vector3.forward());
+ }
+
+ /**
+ * Gets the world-space back vector (+z) of this node.
+ *
+ * @return a new vector that represents the node's back direction in world-space
+ */
+ public final Vector3 getBack() {
+ return localToWorldDirection(Vector3.back());
+ }
+
+ /**
+ * Gets the world-space right vector (+x) of this node.
+ *
+ * @return a new vector that represents the node's right direction in world-space
+ */
+ public final Vector3 getRight() {
+ return localToWorldDirection(Vector3.right());
+ }
+
+ /**
+ * Gets the world-space left vector (-x) of this node.
+ *
+ * @return a new vector that represents the node's left direction in world-space
+ */
+ public final Vector3 getLeft() {
+ return localToWorldDirection(Vector3.left());
+ }
+
+ /**
+ * Gets the world-space up vector (+y) of this node.
+ *
+ * @return a new vector that represents the node's up direction in world-space
+ */
+ public final Vector3 getUp() {
+ return localToWorldDirection(Vector3.up());
+ }
+
+ /**
+ * Gets the world-space down vector (-y) of this node.
+ *
+ * @return a new vector that represents the node's down direction in world-space
+ */
+ public final Vector3 getDown() {
+ return localToWorldDirection(Vector3.down());
+ }
+
+ /**
+ * Sets the {@link Renderable} to display for this node. If {@link
+ * Node#setCollisionShape(CollisionShape)} is not set, then {@link Renderable#getCollisionShape()}
+ * is used to detect collisions for this {@link Node}.
+ *
+ * @see ModelRenderable
+ * @see com.google.ar.sceneform.rendering.ViewRenderable
+ * @param renderable Usually a 3D model. If null, this node's current renderable will be removed.
+ */
+ public void setRenderable(@Nullable Renderable renderable) {
+ AndroidPreconditions.checkUiThread();
+
+ // Renderable hasn't changed, return early.
+ if (renderableInstance != null && renderableInstance.getRenderable() == renderable) {
+ return;
+ }
+
+ if (renderableInstance != null) {
+ if (active) {
+ renderableInstance.detachFromRenderer();
+ }
+ renderableInstance = null;
+ }
+
+ if (renderable != null) {
+ RenderableInstance instance = renderable.createInstance(this);
+ if (active && (scene != null && !scene.isUnderTesting())) {
+ instance.attachToRenderer(getRendererOrDie());
+ }
+ renderableInstance = instance;
+ renderableId = renderable.getId().get();
+ } else {
+ renderableId = ChangeId.EMPTY_ID;
+ }
+
+ refreshCollider();
+ }
+
+ /**
+ * Gets the renderable to display for this node.
+ *
+ * @return renderable to display for this node
+ */
+ @Nullable
+ public Renderable getRenderable() {
+ if (renderableInstance == null) {
+ return null;
+ }
+
+ return renderableInstance.getRenderable();
+ }
+
+ /**
+ * Sets the shape to used to detect collisions for this {@link Node}. If the shape is not set and
+ * {@link Node#setRenderable(Renderable)} is set, then {@link Renderable#getCollisionShape()} is
+ * used to detect collisions for this {@link Node}.
+ *
+ * @see Scene#hitTest(Ray)
+ * @see Scene#hitTestAll(Ray)
+ * @see Scene#overlapTest(Node)
+ * @see Scene#overlapTestAll(Node)
+ * @param collisionShape represents a geometric shape, i.e. sphere, box, convex hull. If null,
+ * this node's current collision shape will be removed.
+ */
+ public void setCollisionShape(@Nullable CollisionShape collisionShape) {
+ AndroidPreconditions.checkUiThread();
+
+ this.collisionShape = collisionShape;
+ refreshCollider();
+ }
+
+ /**
+ * Gets the shape to use for collisions with this node. If the shape is null and {@link
+ * Node#setRenderable(Renderable)} is set, then {@link Renderable#getCollisionShape()} is used to
+ * detect collisions for this {@link Node}.
+ *
+ * @see Scene#hitTest(Ray)
+ * @see Scene#hitTestAll(Ray)
+ * @see Scene#overlapTest(Node)
+ * @see Scene#overlapTestAll(Node)
+ * @return represents a geometric shape, i.e. sphere, box, convex hull.
+ */
+ @Nullable
+ public CollisionShape getCollisionShape() {
+ if (collider != null) {
+ return collider.getShape();
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the {@link Light} to display. To use, first create a {@link Light} using {@link
+ * Light.Builder}. Set the parameters you care about and then attach it to the node using this
+ * function. A node may have a renderable and a light or just act as a {@link Light}.
+ *
+ * @param light Properties of the {@link Light} to render, pass null to remove the light.
+ */
+ public void setLight(@Nullable Light light) {
+ // If this is the same light already set there is nothing to do.
+ if (getLight() == light) {
+ return;
+ }
+
+ // Null-op if the lightInstance is null
+ destroyLightInstance();
+
+ if (light != null) {
+ createLightInstance(light);
+ }
+ }
+
+ /** Gets the current light, which is mutable. */
+ @Nullable
+ public Light getLight() {
+ if (lightInstance != null) {
+ return lightInstance.getLight();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the direction that the node is looking at in world-space. After calling this, {@link
+ * Node#getForward()} will match the look direction passed in. The up direction will determine the
+ * orientation of the node around the direction. The look direction and up direction cannot be
+ * coincident (parallel) or the orientation will be invalid.
+ *
+ * @param lookDirection a vector representing the desired look direction in world-space
+ * @param upDirection a vector representing a valid up vector to use, such as Vector3.up()
+ */
+ public final void setLookDirection(Vector3 lookDirection, Vector3 upDirection) {
+ final Quaternion rotation = Quaternion.lookRotation(lookDirection, upDirection);
+ setWorldRotation(rotation);
+ }
+
+ /**
+ * Sets the direction that the node is looking at in world-space. After calling this, {@link
+ * Node#getForward()} will match the look direction passed in. World-space up (0, 1, 0) will be
+ * used to determine the orientation of the node around the direction.
+ *
+ * @param lookDirection a vector representing the desired look direction in world-space
+ */
+ public final void setLookDirection(Vector3 lookDirection) {
+ // Default up direction
+ Vector3 upDirection = Vector3.up();
+
+ // First determine if the look direction and default up direction are far enough apart to
+ // produce a numerically stable cross product.
+ final float directionUpMatch = Math.abs(Vector3.dot(lookDirection, upDirection));
+ if (directionUpMatch > DIRECTION_UP_EPSILON) {
+ // If the direction vector and up vector coincide choose a new up vector.
+ upDirection = new Vector3(0.0f, 0.0f, 1.0f);
+ }
+
+ // Finally build the rotation with the proper up vector.
+ setLookDirection(lookDirection, upDirection);
+ }
+
+ /** @hide */
+ @Override
+ public final Matrix getWorldModelMatrix() {
+ return getWorldModelMatrixInternal();
+ }
+
+ /**
+ * Handles when this node becomes active. A Node is active if it's enabled, part of a scene, and
+ * its parent is active.
+ *
+ * Override to perform any setup that needs to occur when the node is activated.
+ *
+ * @see #isActive()
+ * @see #isEnabled()
+ */
+ public void onActivate() {
+ // Optionally override.
+ }
+
+ /**
+ * Handles when this node becomes inactivate. A Node is inactive if it's disabled, not part of a
+ * scene, or its parent is inactive.
+ *
+ *
Override to perform any setup that needs to occur when the node is deactivated.
+ *
+ * @see #isActive()
+ * @see #isEnabled()
+ */
+ public void onDeactivate() {
+ // Optionally override.
+ }
+
+ /**
+ * Handles when this node is updated. A node is updated before rendering each frame. This is only
+ * called when the node is active.
+ *
+ *
Override to perform any updates that need to occur each frame.
+ *
+ * @param frameTime provides time information for the current frame
+ */
+ public void onUpdate(FrameTime frameTime) {
+ // Optionally override.
+ }
+
+ /**
+ * Handles when this node is touched.
+ *
+ *
Override to perform any logic that should occur when this node is touched. The way that
+ * touch events are propagated mirrors the way touches are propagated to Android Views. This is
+ * only called when the node is active.
+ *
+ *
When an ACTION_DOWN event occurs, that represents the start of a gesture. ACTION_UP or
+ * ACTION_CANCEL represents when a gesture ends. When a gesture starts, the following is done:
+ *
+ *
+ * - Dispatch touch events to the node that was touched as detected by {@link
+ * Scene#hitTest(MotionEvent)}.
+ *
- If the node doesn't consume the event, recurse upwards through the node's parents and
+ * dispatch the touch event until one of the node's consumes the event.
+ *
- If no nodes consume the event, the gesture is ignored and subsequent events that are part
+ * of the gesture will not be passed to any nodes.
+ *
- If one of the node's consumes the event, then that node will consume all future touch
+ * events for the gesture.
+ *
+ *
+ * When a touch event is dispatched to a node, the event is first passed to the node's {@link
+ * OnTouchListener}. If the {@link OnTouchListener} doesn't handle the event, it is passed to
+ * {@link #onTouchEvent(HitTestResult, MotionEvent)}.
+ *
+ * @param hitTestResult Represents the node that was touched, and information about where it was
+ * touched. On ACTION_DOWN events, {@link HitTestResult#getNode()} will always be this node or
+ * one of its children. On other events, the touch may have moved causing the {@link
+ * HitTestResult#getNode()} to change (or possibly be null).
+ * @param motionEvent The motion event.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean onTouchEvent(HitTestResult hitTestResult, MotionEvent motionEvent) {
+ Preconditions.checkNotNull(hitTestResult, "Parameter \"hitTestResult\" was null.");
+ Preconditions.checkNotNull(motionEvent, "Parameter \"motionEvent\" was null.");
+
+ boolean handled = false;
+
+ // Reset tap tracking data if a new gesture has started or if the Node has become inactive.
+ int actionMasked = motionEvent.getActionMasked();
+ if (actionMasked == MotionEvent.ACTION_DOWN || !isActive()) {
+ tapTrackingData = null;
+ }
+
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ // Only start tacking the tap gesture if there is a tap listener set.
+ // This allows the event to bubble up to the node's parent when there is no listener.
+ if (onTapListener == null) {
+ break;
+ }
+
+ Node hitNode = hitTestResult.getNode();
+ if (hitNode == null) {
+ break;
+ }
+
+ Vector3 downPosition = new Vector3(motionEvent.getX(), motionEvent.getY(), 0.0f);
+ tapTrackingData = new TapTrackingData(hitNode, downPosition);
+ handled = true;
+ break;
+ // For both ACTION_MOVE and ACTION_UP, we need to make sure the tap gesture is still valid.
+ case MotionEvent.ACTION_MOVE:
+ case MotionEvent.ACTION_UP:
+ // Assign to local variable for static analysis.
+ TapTrackingData tapTrackingData = this.tapTrackingData;
+ if (tapTrackingData == null) {
+ break;
+ }
+
+ // Determine how much the touch has moved.
+ float touchSlop = getScaledTouchSlop();
+ Vector3 upPosition = new Vector3(motionEvent.getX(), motionEvent.getY(), 0.0f);
+ float touchDelta = Vector3.subtract(tapTrackingData.downPosition, upPosition).length();
+
+ // Determine if this node or a child node is still being touched.
+ hitNode = hitTestResult.getNode();
+ boolean isHitValid = hitNode == tapTrackingData.downNode;
+
+ // Determine if this is a valid tap.
+ boolean isTapValid = isHitValid || touchDelta < touchSlop;
+ if (isTapValid) {
+ handled = true;
+ // If this is an ACTION_UP event, it's time to call the listener.
+ if (actionMasked == MotionEvent.ACTION_UP && onTapListener != null) {
+ onTapListener.onTap(hitTestResult, motionEvent);
+ this.tapTrackingData = null;
+ }
+ } else {
+ this.tapTrackingData = null;
+ }
+ break;
+ default:
+ // Do nothing.
+ }
+
+ return handled;
+ }
+
+ /**
+ * Handles when this node's transformation is changed.
+ *
+ * The originating node is the most top-level node in the hierarchy that triggered this node to
+ * change. It will always be either the same node or one of its' parents. i.e. if node A's
+ * position is changed, then that will trigger {@link #onTransformChange(Node)} to be called for
+ * all of it's children with the originatingNode being node A.
+ *
+ * @param originatingNode the node that triggered this node's transformation to change
+ */
+ public void onTransformChange(Node originatingNode) {
+ // Optionally Override.
+ }
+
+ /**
+ * Traverses the hierarchy and call a method on each node (including this node). Traversal is
+ * depth first.
+ *
+ * @param consumer the method to call on each node
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ @Override
+ public void callOnHierarchy(Consumer consumer) {
+ consumer.accept(this);
+ super.callOnHierarchy(consumer);
+ }
+
+ /**
+ * Traverses the hierarchy to find the first node (including this node) that meets a condition.
+ * Once the predicate is met, the traversal stops. Traversal is depth first.
+ *
+ * @param condition predicate the defines the conditions of the node to search for.
+ * @return the first node that matches the conditions of the predicate, otherwise null is returned
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ @Override
+ @Nullable
+ public Node findInHierarchy(Predicate condition) {
+ if (condition.test(this)) {
+ return this;
+ }
+
+ return super.findInHierarchy(condition);
+ }
+
+ @Override
+ public String toString() {
+ return name + "(" + super.toString() + ")";
+ }
+
+ /** Returns the parent of this node. */
+ @Nullable
+ final NodeParent getNodeParent() {
+ return parent;
+ }
+
+ @Nullable
+ final Collider getCollider() {
+ return collider;
+ }
+
+ int getNameHash() {
+ return nameHash;
+ }
+
+ /**
+ * Calls onUpdate if the node is active. Used by SceneView to dispatch updates.
+ *
+ * @param frameTime provides time information for the current frame
+ */
+ final void dispatchUpdate(FrameTime frameTime) {
+ if (!isActive()) {
+ return;
+ }
+
+ // Update state when the renderable has changed.
+ Renderable renderable = getRenderable();
+ if (renderable != null && renderable.getId().checkChanged(renderableId)) {
+ // Refresh the collider to ensure it is using the correct collision shape now that the
+ // renderable has changed.
+ refreshCollider();
+ renderableId = renderable.getId().get();
+ }
+
+ onUpdate(frameTime);
+
+ for (LifecycleListener lifecycleListener : lifecycleListeners) {
+ lifecycleListener.onUpdated(this, frameTime);
+ }
+ }
+
+ /**
+ * Calls onTouchEvent if the node is active. Used by TouchEventSystem to dispatch touch events.
+ *
+ * @param hitTestResult Represents the node that was touched, and information about where it was
+ * touched. On ACTION_DOWN events, {@link HitTestResult#getNode()} will always be this node or
+ * one of its children. On other events, the touch may have moved causing the {@link
+ * HitTestResult#getNode()} to change (or possibly be null).
+ * @param motionEvent The motion event.
+ * @return True if the event was handled, false otherwise.
+ */
+ boolean dispatchTouchEvent(HitTestResult hitTestResult, MotionEvent motionEvent) {
+ Preconditions.checkNotNull(hitTestResult, "Parameter \"hitTestResult\" was null.");
+ Preconditions.checkNotNull(motionEvent, "Parameter \"motionEvent\" was null.");
+
+ if (!isActive()) {
+ return false;
+ }
+
+ // TODO: It feels wrong to give Node direct knowledge of Views/ViewRenderable.
+ // It also feels wrong to have a 'Renderable' receive touch events. This hints at a larger
+ // API
+ // problem of Renderable representing more than just rendering information (we have this
+ // problem
+ // with collision shapes too). Investigate a way to refactor this.
+ if (dispatchToViewRenderable(motionEvent)) {
+ return true;
+ }
+
+ if (onTouchListener != null && onTouchListener.onTouch(hitTestResult, motionEvent)) {
+ return true;
+ }
+
+ return onTouchEvent(hitTestResult, motionEvent);
+ }
+
+
+ private boolean dispatchToViewRenderable(MotionEvent motionEvent) {
+ return ViewTouchHelpers.dispatchTouchEventToView(this, motionEvent);
+ }
+
+ /**
+ * WARNING: Do not call this function directly unless you know what you are doing. Sets the scene
+ * field and propagates it to all children recursively. this is called automatically when the node
+ * is added/removed from the scene or its parent changes.
+ *
+ * @param scene The scene to set. If null, the scene is set to null.
+ */
+ final void setSceneRecursively(@Nullable Scene scene) {
+ AndroidPreconditions.checkUiThread();
+
+ // First, set the scene of this node and all child nodes.
+ setSceneRecursivelyInternal(scene);
+
+ // Then, recursively update the active status of this node and all child nodes.
+ updateActiveStatusRecursively();
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // TODO: Gltf animation api should be consistent with Sceneform.
+ @Nullable
+ public RenderableInstance getRenderableInstance() {
+ return renderableInstance;
+ }
+
+ Matrix getLocalModelMatrixInternal() {
+ if ((dirtyTransformFlags & LOCAL_TRANSFORM_DIRTY) == LOCAL_TRANSFORM_DIRTY) {
+ cachedLocalModelMatrix.makeTrs(localPosition, localRotation, localScale);
+ dirtyTransformFlags &= ~LOCAL_TRANSFORM_DIRTY;
+ }
+
+ return cachedLocalModelMatrix;
+ }
+
+ Matrix getWorldModelMatrixInverseInternal() {
+ if ((dirtyTransformFlags & WORLD_INVERSE_TRANSFORM_DIRTY) == WORLD_INVERSE_TRANSFORM_DIRTY) {
+ // Cache the inverse of the world model matrix.
+ // Used for converting from world-space to local-space.
+ Matrix.invert(getWorldModelMatrixInternal(), cachedWorldModelMatrixInverse);
+ dirtyTransformFlags &= ~WORLD_INVERSE_TRANSFORM_DIRTY;
+ }
+
+ return cachedWorldModelMatrixInverse;
+ }
+
+ private void setSceneRecursivelyInternal(@Nullable Scene scene) {
+ this.scene = scene;
+ for (Node node : getChildren()) {
+ node.setSceneRecursively(scene);
+ }
+ }
+
+ private void updateActiveStatusRecursively() {
+ final boolean shouldBeActive = shouldBeActive();
+ if (active != shouldBeActive) {
+ if (shouldBeActive) {
+ activate();
+ } else {
+ deactivate();
+ }
+ }
+
+ for (Node node : getChildren()) {
+ node.updateActiveStatusRecursively();
+ }
+ }
+
+ private boolean shouldBeActive() {
+ if (!enabled) {
+ return false;
+ }
+
+ if (scene == null) {
+ return false;
+ }
+
+ if (parentAsNode != null && !parentAsNode.isActive()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void activate() {
+ AndroidPreconditions.checkUiThread();
+
+ if (active) {
+ // This should NEVER be thrown because updateActiveStatusRecursively checks to make sure
+ // that the active status has changed before calling this. If this exception is thrown, a bug
+ // was introduced.
+ throw new AssertionError("Cannot call activate while already active.");
+ }
+
+ active = true;
+
+ if ((scene != null && !scene.isUnderTesting()) && renderableInstance != null) {
+ renderableInstance.attachToRenderer(getRendererOrDie());
+ }
+
+ if (lightInstance != null) {
+ lightInstance.attachToRenderer(getRendererOrDie());
+ }
+
+ if (collider != null && scene != null) {
+ collider.setAttachedCollisionSystem(scene.collisionSystem);
+ }
+
+ onActivate();
+
+ for (LifecycleListener lifecycleListener : lifecycleListeners) {
+ lifecycleListener.onActivated(this);
+ }
+ }
+
+ private void deactivate() {
+ AndroidPreconditions.checkUiThread();
+
+ if (!active) {
+ // This should NEVER be thrown because updateActiveStatusRecursively checks to make sure
+ // that the active status has changed before calling this. If this exception is thrown, a bug
+ // was introduced.
+ throw new AssertionError("Cannot call deactivate while already inactive.");
+ }
+
+ active = false;
+
+ if (renderableInstance != null) {
+ renderableInstance.detachFromRenderer();
+ }
+
+ if (lightInstance != null) {
+ lightInstance.detachFromRenderer();
+ }
+
+ if (collider != null) {
+ collider.setAttachedCollisionSystem(null);
+ }
+
+ onDeactivate();
+
+ for (LifecycleListener lifecycleListener : lifecycleListeners) {
+ lifecycleListener.onDeactivated(this);
+ }
+ }
+
+ private void dispatchTransformChanged(Node originatingNode) {
+ onTransformChange(originatingNode);
+
+ for (int i = 0; i < transformChangedListeners.size(); i++) {
+ transformChangedListeners.get(i).onTransformChanged(this, originatingNode);
+ }
+ }
+
+ private void refreshCollider() {
+ CollisionShape finalCollisionShape = collisionShape;
+
+ // If no collision shape has been set, fall back to the collision shape from the renderable, if
+ // there is a renderable.
+ Renderable renderable = getRenderable();
+ if (finalCollisionShape == null && renderable != null) {
+ finalCollisionShape = renderable.getCollisionShape();
+ }
+
+ if (finalCollisionShape != null) {
+ // Create the collider if it doesn't already exist.
+ if (collider == null) {
+ collider = new Collider(this, finalCollisionShape);
+
+ // Attach the collider to the collision system if the node is already active.
+ if (active && scene != null) {
+ collider.setAttachedCollisionSystem(scene.collisionSystem);
+ }
+ } else if (collider.getShape() != finalCollisionShape) {
+ // Set the collider's shape to the new shape if needed.
+ collider.setShape(finalCollisionShape);
+ }
+ } else if (collider != null) {
+ // Dispose of the old collider.
+ collider.setAttachedCollisionSystem(null);
+ collider = null;
+ }
+ }
+
+ private int getScaledTouchSlop() {
+ Scene scene = getScene();
+ if (scene == null
+ || !AndroidPreconditions.isAndroidApiAvailable()
+ || AndroidPreconditions.isUnderTesting()) {
+ return DEFAULT_TOUCH_SLOP;
+ }
+
+ SceneView view = scene.getView();
+ ViewConfiguration viewConfiguration = ViewConfiguration.get(view.getContext());
+ return viewConfiguration.getScaledTouchSlop();
+ }
+
+ private Matrix getWorldModelMatrixInternal() {
+ if ((dirtyTransformFlags & WORLD_TRANSFORM_DIRTY) == WORLD_TRANSFORM_DIRTY) {
+ if (parentAsNode == null) {
+ cachedWorldModelMatrix.set(getLocalModelMatrixInternal().data);
+ } else {
+ Matrix.multiply(
+ parentAsNode.getWorldModelMatrixInternal(),
+ getLocalModelMatrixInternal(),
+ cachedWorldModelMatrix);
+ }
+
+ dirtyTransformFlags &= ~WORLD_TRANSFORM_DIRTY;
+ }
+
+ return cachedWorldModelMatrix;
+ }
+
+ /**
+ * Internal Convenience function for accessing cachedWorldPosition that ensures the cached value
+ * is updated before it is accessed. Used internally instead of getWorldPosition because
+ * getWorldPosition is written to be immutable and therefore requires allocating a new Vector for
+ * each use.
+ *
+ * @return The cachedWorldPosition.
+ */
+ private Vector3 getWorldPositionInternal() {
+ if ((dirtyTransformFlags & WORLD_POSITION_DIRTY) == WORLD_POSITION_DIRTY) {
+ if (parentAsNode != null) {
+ getWorldModelMatrixInternal().decomposeTranslation(cachedWorldPosition);
+ } else {
+ cachedWorldPosition.set(localPosition);
+ }
+ dirtyTransformFlags &= ~WORLD_POSITION_DIRTY;
+ }
+
+ return cachedWorldPosition;
+ }
+
+ /**
+ * Internal Convenience function for accessing cachedWorldRotation that ensures the cached value
+ * is updated before it is accessed. Used internally instead of getWorldRotation because
+ * getWorldRotation is written to be immutable and therefore requires allocating a new Quaternion
+ * for each use.
+ *
+ * @return The cachedWorldRotation.
+ */
+ private Quaternion getWorldRotationInternal() {
+ if ((dirtyTransformFlags & WORLD_ROTATION_DIRTY) == WORLD_ROTATION_DIRTY) {
+ if (parentAsNode != null) {
+ getWorldModelMatrixInternal()
+ .decomposeRotation(getWorldScaleInternal(), cachedWorldRotation);
+ } else {
+ cachedWorldRotation.set(localRotation);
+ }
+ dirtyTransformFlags &= ~WORLD_ROTATION_DIRTY;
+ }
+
+ return cachedWorldRotation;
+ }
+
+ /**
+ * Internal Convenience function for accessing cachedWorldScale that ensures the cached value is
+ * updated before it is accessed. Used internally instead of getWorldScale because getWorldScale
+ * is written to be immutable and therefore requires allocating a new Vector3 for each use.
+ *
+ * @return The cachedWorldScale.
+ */
+ private Vector3 getWorldScaleInternal() {
+ if ((dirtyTransformFlags & WORLD_SCALE_DIRTY) == WORLD_SCALE_DIRTY) {
+ if (parentAsNode != null) {
+ getWorldModelMatrixInternal().decomposeScale(cachedWorldScale);
+ } else {
+ cachedWorldScale.set(localScale);
+ }
+ dirtyTransformFlags &= ~WORLD_SCALE_DIRTY;
+ }
+
+ return cachedWorldScale;
+ }
+
+ private void createLightInstance(Light light) {
+ lightInstance = light.createInstance(this);
+ if (lightInstance == null) {
+ throw new NullPointerException("light.createInstance() failed - which should not happen.");
+ }
+ if (active) {
+ lightInstance.attachToRenderer(getRendererOrDie());
+ }
+ }
+
+ private void destroyLightInstance() {
+ // If the light instance is already null, then there is nothing to do so just return.
+ if (lightInstance == null) {
+ return;
+ }
+
+ if (active) {
+ lightInstance.detachFromRenderer();
+ }
+ lightInstance.dispose();
+ lightInstance = null;
+ }
+
+ private Renderer getRendererOrDie() {
+ if (scene == null) {
+ throw new IllegalStateException("Unable to get Renderer.");
+ }
+
+ return Preconditions.checkNotNull(scene.getView().getRenderer());
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/NodeParent.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/NodeParent.java
new file mode 100644
index 0000000..69d549a
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/NodeParent.java
@@ -0,0 +1,216 @@
+package com.google.ar.sceneform;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Base class for all classes that can contain a set of nodes as children.
+ *
+ * The classes {@link Node} and {@link Scene} are both NodeParents. To make a {@link Node} the
+ * child of another {@link Node} or a {@link Scene}, use {@link Node#setParent(NodeParent)}.
+ */
+public abstract class NodeParent {
+ private final ArrayList children = new ArrayList<>();
+ private final List unmodifiableChildren = Collections.unmodifiableList(children);
+
+ // List of children that can be iterated over
+ private final ArrayList iterableChildren = new ArrayList<>();
+
+ // True if the list of children has changed since the last time iterableChildren was updated.
+ private boolean isIterableChildrenDirty;
+
+ // Used to track if the list of iterableChildren is currently being iterated over.
+ // This is an integer instead of a boolean to handle re-entrance (iteration inside of iteration).
+ private int iteratingCounter;
+
+ /** Returns an immutable list of this parent's children. */
+ public final List getChildren() {
+ return unmodifiableChildren;
+ }
+
+ /**
+ * Adds a node as a child of this NodeParent. If the node already has a parent, it is removed from
+ * its old parent. If the node is already a direct child of this NodeParent, no change is made.
+ *
+ * @param child the node to add as a child
+ * @throws IllegalArgumentException if the child is the same object as the parent, or if the
+ * parent is a descendant of the child
+ */
+ public final void addChild(Node child) {
+ Preconditions.checkNotNull(child, "Parameter \"child\" was null.");
+ AndroidPreconditions.checkUiThread();
+
+ // Return early if the parent hasn't changed.
+ if (child.parent == this) {
+ return;
+ }
+
+ StringBuilder failureReason = new StringBuilder();
+ if (!canAddChild(child, failureReason)) {
+ throw new IllegalArgumentException(failureReason.toString());
+ }
+
+ onAddChild(child);
+ }
+
+ /**
+ * Removes a node from the children of this NodeParent. If the node is not a direct child of this
+ * NodeParent, no change is made.
+ *
+ * @param child the node to remove from the children
+ */
+ public final void removeChild(Node child) {
+ Preconditions.checkNotNull(child, "Parameter \"child\" was null.");
+ AndroidPreconditions.checkUiThread();
+
+ // Return early if this parent doesn't contain the child.
+ if (!children.contains(child)) {
+ return;
+ }
+
+ onRemoveChild(child);
+ }
+
+ /**
+ * Traverse the hierarchy and call a method on each node. Traversal is depth first. If this
+ * NodeParent is a Node, traversal starts with this NodeParent, otherwise traversal starts with
+ * its children.
+ *
+ * @param consumer The method to call on each node.
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ public void callOnHierarchy(Consumer consumer) {
+ Preconditions.checkNotNull(consumer, "Parameter \"consumer\" was null.");
+
+ ArrayList iterableChildren = getIterableChildren();
+ startIterating();
+ for (int i = 0; i < iterableChildren.size(); i++) {
+ Node child = iterableChildren.get(i);
+ child.callOnHierarchy(consumer);
+ }
+ stopIterating();
+ }
+
+ /**
+ * Traverse the hierarchy to find the first node that meets a condition. Traversal is depth first.
+ * If this NodeParent is a Node, traversal starts with this NodeParent, otherwise traversal starts
+ * with its children.
+ *
+ * @param condition predicate the defines the conditions of the node to search for.
+ * @return the first node that matches the conditions of the predicate, otherwise null is returned
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ @Nullable
+ public Node findInHierarchy(Predicate condition) {
+ Preconditions.checkNotNull(condition, "Parameter \"condition\" was null.");
+
+ ArrayList iterableChildren = getIterableChildren();
+ Node found = null;
+ startIterating();
+ for (int i = 0; i < iterableChildren.size(); i++) {
+ Node child = iterableChildren.get(i);
+ found = child.findInHierarchy(condition);
+ if (found != null) {
+ break;
+ }
+ }
+ stopIterating();
+ return found;
+ }
+
+ /**
+ * Traverse the hierarchy to find the first node with a given name. Traversal is depth first. If
+ * this NodeParent is a Node, traversal starts with this NodeParent, otherwise traversal starts
+ * with its children.
+ *
+ * @param name The name of the node to find
+ * @return the node if it's found, otherwise null
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ @Nullable
+ public Node findByName(String name) {
+ if (name == null || name.isEmpty()) {
+ return null;
+ }
+
+ int hashToFind = name.hashCode();
+ Node found =
+ findInHierarchy(
+ (node) -> {
+ String nodeName = node.getName();
+ return (node.getNameHash() != 0 && node.getNameHash() == hashToFind)
+ || (nodeName != null && nodeName.equals(name));
+ });
+
+ return found;
+ }
+
+ protected boolean canAddChild(Node child, StringBuilder failureReason) {
+ Preconditions.checkNotNull(child, "Parameter \"child\" was null.");
+ Preconditions.checkNotNull(failureReason, "Parameter \"failureReason\" was null.");
+
+ if (child == this) {
+ failureReason.append("Cannot add child: Cannot make a node a child of itself.");
+ return false;
+ }
+
+ return true;
+ }
+
+ @CallSuper
+ protected void onAddChild(Node child) {
+ Preconditions.checkNotNull(child, "Parameter \"child\" was null.");
+
+ NodeParent previousParent = child.getNodeParent();
+ if (previousParent != null) {
+ previousParent.removeChild(child);
+ }
+
+ children.add(child);
+ child.parent = this;
+
+ isIterableChildrenDirty = true;
+ }
+
+ @CallSuper
+ protected void onRemoveChild(Node child) {
+ Preconditions.checkNotNull(child, "Parameter \"child\" was null.");
+
+ children.remove(child);
+ child.parent = null;
+
+ isIterableChildrenDirty = true;
+ }
+
+ private ArrayList getIterableChildren() {
+ if (isIterableChildrenDirty && !isIterating()) {
+ iterableChildren.clear();
+ iterableChildren.addAll(children);
+ isIterableChildrenDirty = false;
+ }
+
+ return iterableChildren;
+ }
+
+ private void startIterating() {
+ iteratingCounter++;
+ }
+
+ private void stopIterating() {
+ iteratingCounter--;
+ if (iteratingCounter < 0) {
+ throw new AssertionError("stopIteration was called without calling startIteration.");
+ }
+ }
+
+ private boolean isIterating() {
+ return iteratingCounter > 0;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Scene.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Scene.java
new file mode 100644
index 0000000..ba0c256
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Scene.java
@@ -0,0 +1,548 @@
+package com.google.ar.sceneform;
+
+import android.media.Image;
+import android.util.Log;
+import android.view.MotionEvent;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.google.ar.sceneform.collision.Collider;
+import com.google.ar.sceneform.collision.CollisionSystem;
+import com.google.ar.sceneform.collision.Ray;
+import com.google.ar.sceneform.rendering.Color;
+import com.google.ar.sceneform.rendering.LightProbe;
+import com.google.ar.sceneform.rendering.Renderer;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.EnvironmentalHdrParameters;
+import com.google.ar.sceneform.utilities.LoadHelper;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.util.ArrayList;
+
+/**
+ * The Sceneform 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.
+ */
+public class Scene extends NodeParent {
+ /**
+ * Interface definition for a callback to be invoked when a touch event is dispatched to a scene.
+ * The callback will be invoked after the touch event is dispatched to the nodes in the scene if
+ * no node consumed the event.
+ */
+ public interface OnTouchListener {
+ /**
+ * Called when a touch event is dispatched to a scene. The callback will be invoked after the
+ * touch event is dispatched to the nodes in the scene if no node consumed the event. This is
+ * called even if the touch is not over a node, in which case {@link HitTestResult#getNode()}
+ * will be null.
+ *
+ * @see Scene#setOnTouchListener(OnTouchListener)
+ * @param hitTestResult represents the node that was touched
+ * @param motionEvent the motion event
+ * @return true if the listener has consumed the event
+ */
+ boolean onSceneTouch(HitTestResult hitTestResult, MotionEvent motionEvent);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a touch event is dispatched to a scene.
+ * The callback will be invoked before the {@link OnTouchListener} is invoked. This is invoked
+ * even if the gesture was consumed, making it possible to observe all motion events dispatched to
+ * the scene.
+ */
+ public interface OnPeekTouchListener {
+ /**
+ * Called when a touch event is dispatched to a scene. The callback will be invoked before the
+ * {@link OnTouchListener} is invoked. This is invoked even if the gesture was consumed, making
+ * it possible to observe all motion events dispatched to the scene. This is called even if the
+ * touch is not over a node, in which case {@link HitTestResult#getNode()} will be null.
+ *
+ * @see Scene#setOnTouchListener(OnTouchListener)
+ * @param hitTestResult represents the node that was touched
+ * @param motionEvent the motion event
+ */
+ void onPeekTouch(HitTestResult hitTestResult, MotionEvent motionEvent);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked once per frame immediately before the scene
+ * is updated.
+ */
+ public interface OnUpdateListener {
+ /**
+ * Called once per frame right before the Scene is updated.
+ *
+ * @param frameTime provides time information for the current frame
+ */
+ void onUpdate(FrameTime frameTime);
+ }
+
+ private static final String TAG = Scene.class.getSimpleName();
+ private static final String DEFAULT_LIGHTPROBE_ASSET_NAME = "small_empty_house_2k";
+ private static final String DEFAULT_LIGHTPROBE_RESOURCE_NAME = "sceneform_default_light_probe";
+ private static final float DEFAULT_EXPOSURE = 1.0f;
+ public static final EnvironmentalHdrParameters DEFAULT_HDR_PARAMETERS =
+ EnvironmentalHdrParameters.makeDefault();
+
+ private final Camera camera;
+ @Nullable private final Sun sunlightNode;
+ @Nullable private final SceneView view;
+ @Nullable private LightProbe lightProbe;
+ private boolean lightProbeSet = false;
+ private boolean isUnderTesting = false;
+
+ // Systems.
+ final CollisionSystem collisionSystem = new CollisionSystem();
+ private final TouchEventSystem touchEventSystem = new TouchEventSystem();
+
+ private final ArrayList onUpdateListeners = new ArrayList<>();
+
+ @SuppressWarnings("VisibleForTestingUsed")
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ Scene() {
+ view = null;
+ lightProbe = null;
+ camera = new Camera(true);
+ if (!AndroidPreconditions.isMinAndroidApiLevel()) {
+ // Enforce min api level 24
+ sunlightNode = null;
+ } else {
+ sunlightNode = new Sun();
+ }
+
+ isUnderTesting = true;
+ }
+
+ /** Create a scene with the given context. */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Scene(SceneView view) {
+ Preconditions.checkNotNull(view, "Parameter \"view\" was null.");
+ this.view = view;
+ camera = new Camera(this);
+ if (!AndroidPreconditions.isMinAndroidApiLevel()) {
+ // Enforce min api level 24
+ sunlightNode = null;
+ return;
+ }
+ sunlightNode = new Sun(this);
+
+ // Setup the default lighting for the scene, if it exists.
+ setupLightProbe(view);
+ }
+
+ /** Returns the SceneView used to create the scene. */
+ public SceneView getView() {
+ // the view field cannot be marked for the purposes of unit testing.
+ // Add this check for static analysis go/nullness.
+ if (view == null) {
+ throw new IllegalStateException("Scene's view must not be null.");
+ }
+
+ return view;
+ }
+
+ /**
+ * Get the camera that is used to render the scene. The camera is a type of node.
+ *
+ * @return the camera used to render the scene
+ */
+ public Camera getCamera() {
+ return camera;
+ }
+
+ /**
+ * Get the default sunlight node.
+ *
+ * @return the sunlight node used to light the scene
+ */
+ @Nullable
+ public Node getSunlight() {
+ return sunlightNode;
+ }
+
+ /**
+ * Get the Light Probe that defines the lighting environment for the scene.
+ *
+ * @return the light probe used for reflections and indirect lighting.
+ * @hide for 1.0 as we don't yet have tools support
+ */
+ public LightProbe getLightProbe() {
+ // the lightProbe field cannot be marked for the purposes of unit testing.
+ // Add this check for static analysis go/nullness.
+ if (lightProbe == null) {
+ throw new IllegalStateException("Scene's lightProbe must not be null.");
+ }
+ return lightProbe;
+ }
+
+ /**
+ * Set a new Light Probe for the scene, this affects reflections and indirect lighting.
+ *
+ * @param lightProbe the fully loaded LightProbe to be used as the lighting environment.
+ * @hide for 1.0 as we don't yet have tools support
+ */
+ public void setLightProbe(LightProbe lightProbe) {
+ Preconditions.checkNotNull(lightProbe, "Parameter \"lightProbe\" was null.");
+ this.lightProbe = lightProbe;
+ this.lightProbeSet = true;
+
+ // the view field cannot be marked for the purposes of unit testing.
+ // Add this check for static analysis go/nullness.
+ if (view == null) {
+ throw new IllegalStateException("Scene's view must not be null.");
+ }
+ Preconditions.checkNotNull(view.getRenderer()).setLightProbe(lightProbe);
+ }
+
+ /**
+ * Register a callback to be invoked when the scene is touched. The callback will be invoked after
+ * the touch event is dispatched to the nodes in the scene if no node consumed the event. This is
+ * called even if the touch is not over a node, in which case {@link HitTestResult#getNode()} will
+ * be null.
+ *
+ * @param onTouchListener the touch listener to attach
+ */
+ public void setOnTouchListener(@Nullable OnTouchListener onTouchListener) {
+ touchEventSystem.setOnTouchListener(onTouchListener);
+ }
+
+ /**
+ * Adds a listener that will be called before the {@link Scene.OnTouchListener} is invoked. This
+ * is invoked even if the gesture was consumed, making it possible to observe all motion events
+ * dispatched to the scene. This is called even if the touch is not over a node, in which case
+ * {@link HitTestResult#getNode()} will be null. The listeners will be called in the order in
+ * which they were added.
+ *
+ * @param onPeekTouchListener the peek touch listener to add
+ */
+ public void addOnPeekTouchListener(OnPeekTouchListener onPeekTouchListener) {
+ touchEventSystem.addOnPeekTouchListener(onPeekTouchListener);
+ }
+
+ /**
+ * Removes a listener that will be called before the {@link Scene.OnTouchListener} is invoked.
+ * This is invoked even if the gesture was consumed, making it possible to observe all motion
+ * events dispatched to the scene. This is called even if the touch is not over a node, in which
+ * case {@link HitTestResult#getNode()} will be null.
+ *
+ * @param onPeekTouchListener the peek touch listener to remove
+ */
+ public void removeOnPeekTouchListener(OnPeekTouchListener onPeekTouchListener) {
+ touchEventSystem.removeOnPeekTouchListener(onPeekTouchListener);
+ }
+
+ /**
+ * Adds a listener that will be called once per frame immediately before the Scene is updated. The
+ * listeners will be called in the order in which they were added.
+ *
+ * @param onUpdateListener the update listener to add
+ */
+ public void addOnUpdateListener(OnUpdateListener onUpdateListener) {
+ Preconditions.checkNotNull(onUpdateListener, "Parameter 'onUpdateListener' was null.");
+ if (!onUpdateListeners.contains(onUpdateListener)) {
+ onUpdateListeners.add(onUpdateListener);
+ }
+ }
+
+ /**
+ * Removes a listener that will be called once per frame immediately before the Scene is updated.
+ *
+ * @param onUpdateListener the update listener to remove
+ */
+ public void removeOnUpdateListener(OnUpdateListener onUpdateListener) {
+ Preconditions.checkNotNull(onUpdateListener, "Parameter 'onUpdateListener' was null.");
+ onUpdateListeners.remove(onUpdateListener);
+ }
+
+ @Override
+ public void onAddChild(Node child) {
+ super.onAddChild(child);
+ child.setSceneRecursively(this);
+ }
+
+ @Override
+ public void onRemoveChild(Node child) {
+ super.onRemoveChild(child);
+ child.setSceneRecursively(null);
+ }
+
+ /**
+ * Tests to see if a motion event is touching any nodes within the scene, based on a ray hit test
+ * whose origin is the screen position of the motion event, and outputs a HitTestResult containing
+ * the node closest to the screen.
+ *
+ * @param motionEvent the motion event to use for the test
+ * @return the result includes the first node that was hit by the motion event (may be null), and
+ * information about where the motion event hit the node in world-space
+ */
+ public HitTestResult hitTest(MotionEvent motionEvent) {
+ Preconditions.checkNotNull(motionEvent, "Parameter \"motionEvent\" was null.");
+
+ if (camera == null) {
+ return new HitTestResult();
+ }
+
+ Ray ray = camera.motionEventToRay(motionEvent);
+ return hitTest(ray);
+ }
+
+ /**
+ * Tests to see if a ray is hitting any nodes within the scene and outputs a HitTestResult
+ * containing the node closest to the ray origin that intersects with the ray.
+ *
+ * @see Camera#screenPointToRay(float, float)
+ * @param ray the ray to use for the test
+ * @return the result includes the first node that was hit by the ray (may be null), and
+ * information about where the ray hit the node in world-space
+ */
+ public HitTestResult hitTest(Ray ray) {
+ Preconditions.checkNotNull(ray, "Parameter \"ray\" was null.");
+
+ HitTestResult result = new HitTestResult();
+ Collider collider = collisionSystem.raycast(ray, result);
+ if (collider != null) {
+ result.setNode((Node) collider.getTransformProvider());
+ }
+
+ return result;
+ }
+
+ /**
+ * Tests to see if a motion event is touching any nodes within the scene and returns a list of
+ * HitTestResults containing all of the nodes that were hit, sorted by distance.
+ *
+ * @param motionEvent The motion event to use for the test.
+ * @return Populated with a HitTestResult for each node that was hit sorted by distance. Empty if
+ * no nodes were hit.
+ */
+ public ArrayList hitTestAll(MotionEvent motionEvent) {
+ Preconditions.checkNotNull(motionEvent, "Parameter \"motionEvent\" was null.");
+
+ if (camera == null) {
+ return new ArrayList<>();
+ }
+ Ray ray = camera.motionEventToRay(motionEvent);
+ return hitTestAll(ray);
+ }
+
+ /**
+ * Tests to see if a ray is hitting any nodes within the scene and returns a list of
+ * HitTestResults containing all of the nodes that were hit, sorted by distance.
+ *
+ * @see Camera#screenPointToRay(float, float)
+ * @param ray The ray to use for the test.
+ * @return Populated with a HitTestResult for each node that was hit sorted by distance. Empty if
+ * no nodes were hit.
+ */
+ public ArrayList hitTestAll(Ray ray) {
+ Preconditions.checkNotNull(ray, "Parameter \"ray\" was null.");
+
+ ArrayList results = new ArrayList<>();
+
+ collisionSystem.raycastAll(
+ ray,
+ results,
+ (result, collider) -> result.setNode((Node) collider.getTransformProvider()),
+ () -> new HitTestResult());
+
+ return results;
+ }
+
+ /**
+ * Tests to see if the given node's collision shape overlaps the collision shape of any other
+ * nodes in the scene using {@link Node#getCollisionShape()}. The node used for testing does not
+ * need to be active.
+ *
+ * @see #overlapTestAll(Node)
+ * @param node The node to use for the test.
+ * @return A node that is overlapping the test node. If no node is overlapping the test node, then
+ * this is null. If multiple nodes are overlapping the test node, then this could be any of
+ * them.
+ */
+ @Nullable
+ public Node overlapTest(Node node) {
+ Preconditions.checkNotNull(node, "Parameter \"node\" was null.");
+
+ Collider collider = node.getCollider();
+ if (collider == null) {
+ return null;
+ }
+
+ Collider intersectedCollider = collisionSystem.intersects(collider);
+ if (intersectedCollider == null) {
+ return null;
+ }
+
+ return (Node) intersectedCollider.getTransformProvider();
+ }
+
+ /**
+ * Tests to see if a node is overlapping any other nodes within the scene using {@link
+ * Node#getCollisionShape()}. The node used for testing does not need to be active.
+ *
+ * @see #overlapTest(Node)
+ * @param node The node to use for the test.
+ * @return A list of all nodes that are overlapping the test node. If no node is overlapping the
+ * test node, then the list is empty.
+ */
+ public ArrayList overlapTestAll(Node node) {
+ Preconditions.checkNotNull(node, "Parameter \"node\" was null.");
+
+ ArrayList results = new ArrayList<>();
+
+ Collider collider = node.getCollider();
+ if (collider == null) {
+ return results;
+ }
+
+ collisionSystem.intersectsAll(
+ collider,
+ (Collider intersectedCollider) ->
+ results.add((Node) intersectedCollider.getTransformProvider()));
+
+ return results;
+ }
+
+ /** Returns true if this Scene was created by a test. */
+ boolean isUnderTesting() {
+ return isUnderTesting;
+ }
+
+ /**
+ * Sets whether the Scene should expect to use an Hdr light estimate, so that Filament light
+ * settings can be adjusted appropriately.
+ *
+ * @hide intended for use by other Sceneform packages which update Hdr lighting every frame.
+ */
+
+ public void setUseHdrLightEstimate(boolean useHdrLightEstimate) {
+ if (view != null) {
+ Renderer renderer = Preconditions.checkNotNull(view.getRenderer());
+ renderer.setUseHdrLightEstimate(useHdrLightEstimate);
+ }
+ }
+
+ /**
+ * Sets the current Hdr Light Estimate state to apply to the Filament scene.
+ *
+ * @hide intended for use by other Sceneform packages which update Hdr lighting every frame.
+ */
+ // incompatible types in argument.
+ @SuppressWarnings("nullness:argument.type.incompatible")
+
+ public void setEnvironmentalHdrLightEstimate(
+ @Nullable float[] sphericalHarmonics,
+ @Nullable float[] direction,
+ Color colorCorrection,
+ float relativeIntensity,
+ @Nullable Image[] cubeMap) {
+ float exposure;
+ EnvironmentalHdrParameters hdrParameters;
+ if (view == null) {
+ exposure = DEFAULT_EXPOSURE;
+ hdrParameters = DEFAULT_HDR_PARAMETERS;
+ } else {
+ Renderer renderer = Preconditions.checkNotNull(view.getRenderer());
+ exposure = renderer.getExposure();
+ hdrParameters = renderer.getEnvironmentalHdrParameters();
+ }
+
+ if (lightProbe != null) {
+ if (sphericalHarmonics != null) {
+ lightProbe.setEnvironmentalHdrSphericalHarmonics(
+ sphericalHarmonics, exposure, hdrParameters);
+ }
+ if (cubeMap != null) {
+ lightProbe.setCubeMap(cubeMap);
+ }
+ setLightProbe(lightProbe);
+ }
+ if (sunlightNode != null && direction != null) {
+ sunlightNode.setEnvironmentalHdrLightEstimate(
+ direction, colorCorrection, relativeIntensity, exposure, hdrParameters);
+ }
+ }
+
+ /**
+ * Sets light estimate to modulate the scene lighting and intensity. The rendered lights will use
+ * a combination of these values and the color and intensity of the lights. A value of a white
+ * colorCorrection and pixelIntensity of 1 mean that no changes are made to the light settings.
+ *
+ * This is used by AR Sceneform scenes internally to adjust lighting based on values from
+ * ARCore. An AR scene will call this automatically, possibly overriding other settings. In most
+ * cases, you should not need to call this explicitly.
+ *
+ * @param colorCorrection modulates the lighting color of the scene.
+ * @param pixelIntensity modulates the lighting intensity of the scene.
+ */
+ public void setLightEstimate(Color colorCorrection, float pixelIntensity) {
+ if (lightProbe != null) {
+ lightProbe.setLightEstimate(colorCorrection, pixelIntensity);
+ // TODO: The following call is not public (@hide). When public, ensure that it is
+ // not possible to forget to call setLightProbe after changing the light estimate of a light
+ // probe.
+ setLightProbe(lightProbe);
+ }
+ if (sunlightNode != null) {
+ sunlightNode.setLightEstimate(colorCorrection, pixelIntensity);
+ }
+ }
+
+ void onTouchEvent(MotionEvent motionEvent) {
+ Preconditions.checkNotNull(motionEvent, "Parameter \"motionEvent\" was null.");
+
+ // TODO: Investigate API for controlling what node's can be hit by the hitTest.
+ // i.e. layers, disabling collision shapes.
+ HitTestResult hitTestResult = hitTest(motionEvent);
+ touchEventSystem.onTouchEvent(hitTestResult, motionEvent);
+ }
+
+ void dispatchUpdate(FrameTime frameTime) {
+ for (OnUpdateListener onUpdateListener : onUpdateListeners) {
+ onUpdateListener.onUpdate(frameTime);
+ }
+
+ callOnHierarchy(node -> node.dispatchUpdate(frameTime));
+ }
+
+ @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
+ private void setupLightProbe(SceneView view) {
+ Preconditions.checkNotNull(view, "Parameter \"view\" was null.");
+
+ int defaultLightProbeId =
+ LoadHelper.rawResourceNameToIdentifier(view.getContext(), DEFAULT_LIGHTPROBE_RESOURCE_NAME);
+
+ if (defaultLightProbeId == LoadHelper.INVALID_RESOURCE_IDENTIFIER) {
+ // TODO: Better log message.
+ Log.w(
+ TAG,
+ "Unable to find the default Light Probe."
+ + " The scene will not be lit unless a light probe is set.");
+ return;
+ }
+
+ try {
+ LightProbe.builder()
+ .setSource(view.getContext(), defaultLightProbeId)
+ .setAssetName(DEFAULT_LIGHTPROBE_ASSET_NAME)
+ .build()
+ .thenAccept(
+ result -> {
+ // Set when setLightProbe is called so that we don't override the user setting.
+ if (!lightProbeSet) {
+ setLightProbe(result);
+ }
+ })
+ .exceptionally(
+ throwable -> {
+ Log.e(TAG, "Failed to create the default Light Probe: ", throwable);
+ return null;
+ });
+ } catch (Exception ex) {
+ throw new IllegalStateException(
+ "Failed to create the default Light Probe: " + ex.getLocalizedMessage());
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/SceneView.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/SceneView.java
new file mode 100644
index 0000000..04526f4
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/SceneView.java
@@ -0,0 +1,424 @@
+package com.google.ar.sceneform;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceView;
+import androidx.annotation.Nullable;
+
+import com.google.ar.core.exceptions.CameraNotAvailableException;
+
+
+import com.google.ar.sceneform.rendering.Color;
+import com.google.ar.sceneform.rendering.Renderer;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.MovingAverageMillisecondsTracker;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+
+/** A Sceneform SurfaceView that manages rendering and interaction with the scene. */
+public class SceneView extends SurfaceView implements Choreographer.FrameCallback {
+ private static final String TAG = SceneView.class.getSimpleName();
+
+ @Nullable private Renderer renderer = null;
+ private final FrameTime frameTime = new FrameTime();
+
+ private Scene scene;
+ private volatile boolean debugEnabled = false;
+
+ private boolean isInitialized = false;
+
+ @Nullable private Color backgroundColor;
+
+
+
+
+
+ // Used to track high-level performance metrics for Sceneform
+ private final MovingAverageMillisecondsTracker frameTotalTracker =
+ new MovingAverageMillisecondsTracker();
+ private final MovingAverageMillisecondsTracker frameUpdateTracker =
+ new MovingAverageMillisecondsTracker();
+ private final MovingAverageMillisecondsTracker frameRenderTracker =
+ new MovingAverageMillisecondsTracker();
+
+ /**
+ * Defines a transform from {@link Choreographer} time to animation time. Used to control the
+ * playback of animations in a {@link SceneView}.
+ */
+ public interface AnimationTimeTransformer {
+ /**
+ * Transforms nanosecond times generated from the {@link Choreographer} to generate the
+ * animation update time. The input nano time can be used to ensure that returned times never
+ * decrease.
+ *
+ * @see {@link SceneView#setAnimationTimeTransformer(AnimationTimeTransformer)}
+ * @param choreographerTime the current frame time returned from the {@link Choreographer}.
+ */
+ long getAnimationTime(long choreographerTime);
+ }
+
+ private AnimationTimeTransformer animationTimeTransformer = frameTime -> frameTime;
+
+ /**
+ * Constructs a SceneView object and binds it to an Android Context.
+ *
+ * @see #SceneView(Context, AttributeSet)
+ * @param context the Android Context to use
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public SceneView(Context context) {
+ super(context);
+ initialize();
+ }
+
+ /**
+ * Constructs a SceneView object and binds it to an Android Context.
+ *
+ * @param context the Android Context to use
+ * @param attrs the Android AttributeSet to associate with
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public SceneView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouchEvent(MotionEvent motionEvent) {
+ // this makes sure that the view's onTouchListener is called.
+ if (!super.onTouchEvent(motionEvent)) {
+ scene.onTouchEvent(motionEvent);
+ // We must always return true to guarantee that this view will receive all touch events.
+ // TODO: Update Scene.onTouchEvent to return if it was handled.
+
+ return true;
+ }
+ return true;
+ }
+
+ /**
+ * Set the background to a given {@link Drawable}, or remove the background. If the background is
+ * a {@link ColorDrawable}, then the background color of the {@link Scene} is set to {@link
+ * ColorDrawable#getColor()} (the alpha of the color is ignored). Otherwise, default to the
+ * behavior of {@link SurfaceView#setBackground(Drawable)}.
+ */
+ @Override
+ public void setBackground(@Nullable Drawable background) {
+ if (background instanceof ColorDrawable) {
+ ColorDrawable colorDrawable = (ColorDrawable) background;
+ backgroundColor = new Color(colorDrawable.getColor());
+ if (renderer != null) {
+ renderer.setClearColor(backgroundColor.inverseTonemap());
+ }
+ } else {
+ backgroundColor = null;
+ if (renderer != null) {
+ renderer.setDefaultClearColor();
+ }
+ super.setBackground(background);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ int width = right - left;
+ int height = bottom - top;
+ Preconditions.checkNotNull(renderer).setDesiredSize(width, height);
+ }
+
+ /**
+ * Resume Sceneform, which resumes the rendering thread.
+ *
+ *
Typically called from onResume().
+ *
+ * @throws CameraNotAvailableException
+ */
+ public void resume() throws CameraNotAvailableException {
+ if (renderer != null) {
+ renderer.onResume();
+ }
+ // Start the drawing when the renderer is resumed. Remove and re-add the callback
+ // to avoid getting called twice.
+ Choreographer.getInstance().removeFrameCallback(this);
+ Choreographer.getInstance().postFrameCallback(this);
+ }
+
+ /**
+ * Pause Sceneform, which pauses the rendering thread.
+ *
+ *
Typically called from onPause().
+ */
+ public void pause() {
+ Choreographer.getInstance().removeFrameCallback(this);
+ if (renderer != null) {
+ renderer.onPause();
+ }
+ }
+
+ /**
+ * Required to exit Sceneform.
+ *
+ *
Typically called from onDestroy().
+ */
+ public void destroy() {
+ if (renderer != null) {
+ renderer.dispose();
+ renderer = null;
+ }
+ }
+
+ /**
+ * Immediately releases all rendering resources, even if in use.
+ *
+ *
Use this if nothing more will be rendered in this scene or any other, and the memory must be
+ * released immediately.
+ */
+
+ public static void destroyAllResources() {
+ Renderer.destroyAllResources();
+ }
+
+ /**
+ * Releases rendering resources ready for garbage collection
+ *
+ *
Called every frame to collect unused resources. May be called manually to release resources
+ * after rendering has stopped.
+ *
+ * @return Count of resources currently in use
+ */
+
+ public static long reclaimReleasedResources() {
+ return Renderer.reclaimReleasedResources();
+ }
+
+ /**
+ * If enabled, provides various visualizations for debugging.
+ *
+ * @param enable True to enable debugging visualizations, false to disable it.
+ */
+ public void enableDebug(boolean enable) {
+ debugEnabled = enable;
+ }
+
+ /** Indicates whether debugging is enabled for this view. */
+ public boolean isDebugEnabled() {
+ return debugEnabled;
+ }
+
+ /**
+ * Returns the renderer used for this view, or null if the renderer is not setup.
+ *
+ * @hide Not a public facing API for version 1.0
+ */
+ @Nullable
+ public Renderer getRenderer() {
+ return renderer;
+ }
+
+ /** Returns the Sceneform Scene created by this view. */
+ public Scene getScene() {
+ return scene;
+ }
+
+ /**
+ * To capture the contents of this view, designate a {@link Surface} onto which this SceneView
+ * should be mirrored. Use {@link android.media.MediaRecorder#getSurface()}, {@link
+ * android.media.MediaCodec#createInputSurface()} or {@link
+ * android.media.MediaCodec#createPersistentInputSurface()} to obtain the input surface for
+ * recording. This will incur a rendering performance cost and should only be set when capturing
+ * this view. To stop the additional rendering, call stopMirroringToSurface.
+ *
+ * @param surface the Surface onto which the rendered scene should be mirrored.
+ * @param left the left edge of the rectangle into which the view should be mirrored on surface.
+ * @param bottom the bottom edge of the rectangle into which the view should be mirrored on
+ * surface.
+ * @param width the width of the rectangle into which the SceneView should be mirrored on surface.
+ * @param height the height of the rectangle into which the SceneView should be mirrored on
+ * surface.
+ */
+ public void startMirroringToSurface(
+ Surface surface, int left, int bottom, int width, int height) {
+ if (renderer != null) {
+ renderer.startMirroring(surface, left, bottom, width, height);
+ }
+ }
+
+ /**
+ * When capturing is complete, call this method to stop mirroring the SceneView to the specified
+ * {@link Surface}. If this is not called, the additional performance cost will remain.
+ *
+ *
The application is responsible for calling {@link Surface#release()} on the Surface when
+ * done.
+ */
+ public void stopMirroringToSurface(Surface surface) {
+ if (renderer != null) {
+ renderer.stopMirroring(surface);
+ }
+ }
+
+ /**
+ * Initialize the renderer. This creates the Renderer and sets the camera.
+ *
+ * @see #SceneView(Context, AttributeSet)
+ */
+ private void initialize() {
+ if (isInitialized) {
+ Log.w(TAG, "SceneView already initialized.");
+ return;
+ }
+
+ if (!AndroidPreconditions.isMinAndroidApiLevel()) {
+ Log.e(TAG, "Sceneform requires Android N or later");
+ renderer = null;
+ } else {
+ renderer = new Renderer(this);
+ if (backgroundColor != null) {
+ renderer.setClearColor(backgroundColor.inverseTonemap());
+ }
+ scene = new Scene(this);
+ renderer.setCameraProvider(scene.getCamera());
+ initializeAnimation();
+ }
+ isInitialized = true;
+ }
+
+ /**
+ * Update view-specific logic before for each display frame.
+ *
+ * @return true if the scene should be updated before rendering.
+ * @hide
+ */
+ protected boolean onBeginFrame(long frameTimeNanos) {
+ return true;
+ }
+
+ /**
+ * Callback that occurs for each display frame. Updates the scene and reposts itself to be called
+ * by the choreographer on the next frame.
+ *
+ * @hide
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ // Always post the callback for the next frame.
+ Choreographer.getInstance().postFrameCallback(this);
+ doFrameNoRepost(frameTimeNanos);
+ }
+
+ /**
+ * Callback that occurs for each display frame. Updates the scene but does not post a callback
+ * request to the choreographer for the next frame. This is used for testing where on-demand
+ * renders are needed.
+ *
+ * @hide
+ */
+ public void doFrameNoRepost(long frameTimeNanos) {
+ // TODO: Display the tracked performance metrics in debug mode.
+ if (debugEnabled) {
+ frameTotalTracker.beginSample();
+ }
+
+ if (onBeginFrame(frameTimeNanos)) {
+ doUpdate(frameTimeNanos);
+ doRender(frameTimeNanos);
+ }
+
+ if (debugEnabled) {
+ frameTotalTracker.endSample();
+ if ((System.currentTimeMillis() / 1000) % 60 == 0) {
+ Log.d(TAG, " PERF COUNTER: frameRender: " + frameRenderTracker.getAverage());
+ Log.d(TAG, " PERF COUNTER: frameTotal: " + frameTotalTracker.getAverage());
+ Log.d(TAG, " PERF COUNTER: frameUpdate: " + frameUpdateTracker.getAverage());
+ }
+ }
+ }
+
+ private void doUpdate(long frameTimeNanos) {
+ if (debugEnabled) {
+ frameUpdateTracker.beginSample();
+ }
+
+ frameTime.update(frameTimeNanos);
+
+ // Update the AnimationEngine, this should be done before the hierarchy is updated
+ // in case any nodes are following the position of bones in the future.
+ updateAnimation(frameTimeNanos);
+
+ scene.dispatchUpdate(frameTime);
+
+ if (debugEnabled) {
+ frameUpdateTracker.endSample();
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private void updateAnimation(long frameTimeNanos) {return ;}
+
+
+
+
+
+
+ private void doRender(long frameTimeNanos) {
+ Renderer renderer = this.renderer;
+ if (renderer == null) {
+ return;
+ }
+
+ if (debugEnabled) {
+ frameRenderTracker.beginSample();
+ }
+
+ renderer.render(debugEnabled, frameTimeNanos);
+
+ if (debugEnabled) {
+ frameRenderTracker.endSample();
+ }
+ }
+
+
+ private void initializeAnimation() {return ;}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/SequentialTask.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/SequentialTask.java
new file mode 100644
index 0000000..13b77cb
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/SequentialTask.java
@@ -0,0 +1,47 @@
+package com.google.ar.sceneform;
+
+import android.annotation.TargetApi;
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+/**
+ * Executes multiple {@link Runnable}s sequentially by appending them to a {@link
+ * CompletableFuture}.
+ *
+ *
This should only be modified on the main thread.
+ */
+@TargetApi(24)
+@SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
+class SequentialTask {
+ @Nullable private CompletableFuture future;
+
+ /**
+ * Appends a new Runnable to the current future, or creates a new one.
+ *
+ * @return The current future.
+ */
+ @MainThread
+ public CompletableFuture appendRunnable(Runnable action, Executor executor) {
+ if (future != null && !future.isDone()) {
+ future = future.thenRunAsync(action, executor);
+ } else {
+ future = CompletableFuture.runAsync(action, executor);
+ }
+ return future;
+ }
+
+ /** True if the future is null or done. */
+ @MainThread
+ public boolean isDone() {
+ if (future == null) {
+ return true;
+ }
+ if (future.isDone()) {
+ future = null;
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Sun.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Sun.java
new file mode 100644
index 0000000..787e872
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/Sun.java
@@ -0,0 +1,144 @@
+package com.google.ar.sceneform;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.google.ar.sceneform.math.Quaternion;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.Color;
+import com.google.ar.sceneform.rendering.Light;
+import com.google.ar.sceneform.utilities.EnvironmentalHdrParameters;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Represents the "sun" - the default directional light in the scene.
+ *
+ * The following method will throw {@link UnsupportedOperationException} when called: {@link
+ * #setParent(NodeParent)} - Sunlight's parent cannot be changed, it is always the scene.
+ *
+ *
All other functionality in Node is supported. You can access the position and rotation of the
+ * sun, assign a collision shape to the sun, or add children to the sun. Disabling the sun turns off
+ * the default directional light.
+ */
+public class Sun extends Node {
+ @ColorInt static final int DEFAULT_SUNLIGHT_COLOR = 0xfff2d3c4;
+ static final Vector3 DEFAULT_SUNLIGHT_DIRECTION = new Vector3(0.7f, -1.0f, -0.8f);
+
+ // The Light estimate scale and offset allow the final change in intensity to be controlled to
+ // avoid over darkening or changes that are too drastic: appliedEstimate = estimate*scale + offset
+ private static final float LIGHT_ESTIMATE_SCALE = 1.8f;
+ private static final float LIGHT_ESTIMATE_OFFSET = 0.0f;
+ private float baseIntensity = 0.0f;
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ Sun() {}
+
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ Sun(Scene scene) {
+ super();
+ Preconditions.checkNotNull(scene, "Parameter \"scene\" was null.");
+ super.setParent(scene);
+
+ setupDefaultLighting(scene.getView());
+ }
+
+ @Override
+ public void setParent(@Nullable NodeParent parent) {
+ throw new UnsupportedOperationException(
+ "Sun's parent cannot be changed, it is always the scene.");
+ }
+
+ /**
+ * Applies the Environmental HDR light estimate to the directional light
+ *
+ *
The exposure used here is calculated as 1.0f / (1.2f * aperture^2 / shutter speed * 100.0f /
+ * iso);
+ *
+ * @param direction directional light orientation as returned from light estimation.
+ * @param color relative color returned from light estimation.
+ * @param environmentalHdrIntensity maximum intensity from light estimation.
+ * @param exposure Exposure value from Filament.
+ * @hide intended for use by other Sceneform packages which update Hdr lighting every frame.
+ */
+
+ void setEnvironmentalHdrLightEstimate(
+ float[] direction,
+ Color color,
+ float environmentalHdrIntensity,
+ float exposure,
+ EnvironmentalHdrParameters environmentalHdrParameters) {
+ Light light = getLight();
+ if (light == null) {
+ return;
+ }
+
+ // Convert from Environmetal hdr's relative value to lux for filament using hard coded value.
+ float filamentIntensity =
+ environmentalHdrIntensity
+ * environmentalHdrParameters.getDirectIntensityForFilament()
+ / exposure;
+
+ light.setColor(color);
+ light.setIntensity(filamentIntensity);
+
+ // If light is detected as shining up from below, we flip the Y component so that we always end
+ // up with a shadow on the ground to fulfill UX requirements.
+ Vector3 lookDirection =
+ new Vector3(-direction[0], -Math.abs(direction[1]), -direction[2]).normalized();
+ Quaternion lookRotation = Quaternion.rotationBetweenVectors(Vector3.forward(), lookDirection);
+ setWorldRotation(lookRotation);
+ }
+
+ void setLightEstimate(Color colorCorrection, float pixelIntensity) {
+ Light light = getLight();
+ if (light == null) {
+ return;
+ }
+
+ // If we don't know the base intensity of the light, get it now.
+ if (baseIntensity == 0.0f) {
+ baseIntensity = light.getIntensity();
+ }
+
+ // Scale and bias the estimate to avoid over darkening.
+ float lightIntensity =
+ baseIntensity
+ * Math.min(pixelIntensity * LIGHT_ESTIMATE_SCALE + LIGHT_ESTIMATE_OFFSET, 1.0f);
+
+ // Modulates sun color by color correction.
+ Color lightColor = new Color(DEFAULT_SUNLIGHT_COLOR);
+ lightColor.r *= colorCorrection.r;
+ lightColor.g *= colorCorrection.g;
+ lightColor.b *= colorCorrection.b;
+
+ // Modifies light color and intensity by light estimate.
+ light.setColor(lightColor);
+ light.setIntensity(lightIntensity);
+ }
+
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ private void setupDefaultLighting(SceneView view) {
+ Preconditions.checkNotNull(view, "Parameter \"view\" was null.");
+
+ final Color sunlightColor = new Color(DEFAULT_SUNLIGHT_COLOR);
+ if (sunlightColor == null) {
+ throw new AssertionError("Sunlight color is null.");
+ }
+
+ // Set the Node direction to point the sunlight in the desired direction.
+ setLookDirection(DEFAULT_SUNLIGHT_DIRECTION.normalized());
+
+ // Create and set the directional light.
+ Light sunlight =
+ Light.builder(Light.Type.DIRECTIONAL)
+ .setColor(sunlightColor)
+ .setShadowCastingEnabled(true)
+ .build();
+
+ if (sunlight == null) {
+ throw new AssertionError("Failed to create the default sunlight.");
+ }
+ this.setLight(sunlight);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/TouchEventSystem.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/TouchEventSystem.java
new file mode 100644
index 0000000..44326c7
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/TouchEventSystem.java
@@ -0,0 +1,353 @@
+package com.google.ar.sceneform;
+
+import android.view.MotionEvent;
+import androidx.annotation.Nullable;
+import com.google.ar.sceneform.Scene.OnPeekTouchListener;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+
+/**
+ * Manages propagation of touch events to node's within a scene.
+ *
+ *
The way that touch events are propagated mirrors the way touches are propagated to Android
+ * Views.
+ *
+ *
When an ACTION_DOWN event occurs, that represents that start of a gesture. ACTION_UP or
+ * ACTION_CANCEL represents when a gesture ends. When a gesture starts, the following is done:
+ *
+ *
+ * - Call {@link Node#dispatchTouchEvent(HitTestResult, MotionEvent)} on the node that was
+ * touched as detected by scene.hitTest.
+ *
- If {@link Node#dispatchTouchEvent(HitTestResult, MotionEvent)} returns false, recurse
+ * upwards through the node's parents and call {@link Node#dispatchTouchEvent(HitTestResult,
+ * MotionEvent)} until one of the node's returns true.
+ *
- If every node returns false, the gesture is ignored and subsequent events that are part of
+ * the gesture will not be passed to any nodes.
+ *
- If one of the node's returns true, then that node will receive all future touch events for
+ * the gesture.
+ *
+ *
+ * @hide
+ */
+public class TouchEventSystem {
+ private Method motionEventSplitMethod;
+ private final Object[] motionEventSplitParams = new Object[1];
+
+ /**
+ * Keeps track of which nodes are handling events for which pointer Id's. Implemented as a linked
+ * list to store an ordered list of touch targets.
+ */
+ private static class TouchTarget {
+ public static final int ALL_POINTER_IDS = -1; // all ones
+
+ // The touch target.
+ public Node node;
+
+ // The combined bit mask of pointer ids for all pointers captured by the target.
+ public int pointerIdBits;
+
+ // The next target in the target list.
+ @Nullable public TouchTarget next;
+ }
+
+ @Nullable private Scene.OnTouchListener onTouchListener;
+ private final ArrayList onPeekTouchListeners = new ArrayList<>();
+
+ // The touch listener that is handling the current gesture.
+ @Nullable private Scene.OnTouchListener handlingTouchListener = null;
+
+ // Linked list of nodes that are currently handling touches for a set of pointers.
+ @Nullable private TouchTarget firstHandlingTouchTarget = null;
+
+ public TouchEventSystem() {}
+
+ /**
+ * Get the currently registered callback for touch events.
+ *
+ * @see #setOnTouchListener(Scene.OnTouchListener)
+ * @return the attached touch listener
+ */
+ @Nullable
+ public Scene.OnTouchListener getOnTouchListener() {
+ return onTouchListener;
+ }
+
+ /**
+ * Register a callback to be invoked when the scene is touched. The callback is invoked before any
+ * node receives the event. If the callback handles the event, then the gesture is never received
+ * by the nodes.
+ *
+ * @param onTouchListener the touch listener to attach
+ */
+ public void setOnTouchListener(@Nullable Scene.OnTouchListener onTouchListener) {
+ this.onTouchListener = onTouchListener;
+ }
+
+ /**
+ * Adds a listener that will be called before the {@link Scene.OnTouchListener} is invoked. This
+ * is invoked even if the gesture was consumed, making it possible to observe all motion events
+ * dispatched to the scene. This is called even if the touch is not over a node, in which case
+ * {@link HitTestResult#getNode()} will be null. The listeners will be called in the order in
+ * which they were added.
+ *
+ * @param onPeekTouchListener the peek touch listener to add
+ */
+ public void addOnPeekTouchListener(OnPeekTouchListener onPeekTouchListener) {
+ if (!onPeekTouchListeners.contains(onPeekTouchListener)) {
+ onPeekTouchListeners.add(onPeekTouchListener);
+ }
+ }
+
+ /**
+ * Removes a listener that will be called before the {@link Scene.OnTouchListener} is invoked.
+ * This is invoked even if the gesture was consumed, making it possible to observe all motion
+ * events dispatched to the scene. This is called even if the touch is not over a node, in which
+ * case {@link HitTestResult#getNode()} will be null.
+ *
+ * @param onPeekTouchListener the peek touch listener to remove
+ */
+ public void removeOnPeekTouchListener(OnPeekTouchListener onPeekTouchListener) {
+ onPeekTouchListeners.remove(onPeekTouchListener);
+ }
+
+ public void onTouchEvent(HitTestResult hitTestResult, MotionEvent motionEvent) {
+ Preconditions.checkNotNull(hitTestResult, "Parameter \"hitTestResult\" was null.");
+ Preconditions.checkNotNull(motionEvent, "Parameter \"motionEvent\" was null.");
+
+ int actionMasked = motionEvent.getActionMasked();
+
+ // This is a brand new gesture, so clear everything.
+ if (actionMasked == MotionEvent.ACTION_DOWN) {
+ clearTouchTargets();
+ }
+
+ // Dispatch touch event to the peek touch listener, which reveives all events even if the
+ // gesture is being consumed.
+ for (OnPeekTouchListener onPeekTouchListener : onPeekTouchListeners) {
+ onPeekTouchListener.onPeekTouch(hitTestResult, motionEvent);
+ }
+
+ // If the touch listener is already handling the gesture, always dispatch to it.
+ if (handlingTouchListener != null) {
+ tryDispatchToSceneTouchListener(hitTestResult, motionEvent);
+ } else {
+
+ TouchTarget newTouchTarget = null;
+ boolean alreadyDispatchedToNewTouchTarget = false;
+ boolean alreadyDispatchedToAnyTarget = false;
+ Node hitNode = hitTestResult.getNode();
+
+ // New pointer has touched the scene.
+ // Find the appropriate touch target for this pointer.
+ if ((actionMasked == MotionEvent.ACTION_DOWN
+ || (actionMasked == MotionEvent.ACTION_POINTER_DOWN))) {
+ int actionIndex = motionEvent.getActionIndex();
+ int idBitsToAssign = 1 << motionEvent.getPointerId(actionIndex);
+
+ // Clean up earlier touch targets for this pointer id in case they have become out of sync.
+ removePointersFromTouchTargets(idBitsToAssign);
+
+ // See if this event occurred on a node that is already a touch target.
+ if (hitNode != null) {
+ newTouchTarget = getTouchTargetForNode(hitNode);
+ if (newTouchTarget != null) {
+ // Give the existing touch target the new pointer in addition to the one it is handling.
+ newTouchTarget.pointerIdBits |= idBitsToAssign;
+ } else {
+ Node handlingNode =
+ dispatchTouchEvent(motionEvent, hitTestResult, hitNode, idBitsToAssign, true);
+ if (handlingNode != null) {
+ newTouchTarget = addTouchTarget(handlingNode, idBitsToAssign);
+ alreadyDispatchedToNewTouchTarget = true;
+ }
+ alreadyDispatchedToAnyTarget = true;
+ }
+ }
+
+ if (newTouchTarget == null && firstHandlingTouchTarget != null) {
+ // did not find an existing target to receive the event.
+ // Assign the pointer to the least recently added target.
+ newTouchTarget = firstHandlingTouchTarget;
+ while (newTouchTarget.next != null) {
+ newTouchTarget = newTouchTarget.next;
+ }
+ newTouchTarget.pointerIdBits |= idBitsToAssign;
+ }
+ }
+
+ // Dispatch event to touch targets.
+ if (firstHandlingTouchTarget != null) {
+ TouchTarget target = firstHandlingTouchTarget;
+ while (target != null) {
+ TouchTarget next = target.next;
+ if (!alreadyDispatchedToNewTouchTarget || target != newTouchTarget) {
+ dispatchTouchEvent(
+ motionEvent, hitTestResult, target.node, target.pointerIdBits, false);
+ }
+ target = next;
+ }
+ } else if (!alreadyDispatchedToAnyTarget) {
+ tryDispatchToSceneTouchListener(hitTestResult, motionEvent);
+ }
+ }
+
+ if (actionMasked == MotionEvent.ACTION_CANCEL || actionMasked == MotionEvent.ACTION_UP) {
+ clearTouchTargets();
+ } else if (actionMasked == MotionEvent.ACTION_POINTER_UP) {
+ int actionIndex = motionEvent.getActionIndex();
+ int idBitsToRemove = 1 << motionEvent.getPointerId(actionIndex);
+ removePointersFromTouchTargets(idBitsToRemove);
+ }
+ }
+
+ private boolean tryDispatchToSceneTouchListener(
+ HitTestResult hitTestResult, MotionEvent motionEvent) {
+ // This is a new gesture, give the touch listener a chance to capture the input.
+ if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ // If the listener handles the gesture, then the event is never propagated to the nodes.
+ if (onTouchListener != null && onTouchListener.onSceneTouch(hitTestResult, motionEvent)) {
+ // The touch listener is handling the gesture, return early.
+ handlingTouchListener = onTouchListener;
+ return true;
+ }
+ } else if (handlingTouchListener != null) {
+ handlingTouchListener.onSceneTouch(hitTestResult, motionEvent);
+ return true;
+ }
+
+ return false;
+ }
+
+ private MotionEvent splitMotionEvent(MotionEvent motionEvent, int idBits) {
+ if (motionEventSplitMethod == null) {
+ try {
+ Class motionEventClass = MotionEvent.class;
+ motionEventSplitMethod = motionEventClass.getMethod("split", int.class);
+ } catch (ReflectiveOperationException ex) {
+ throw new RuntimeException("Splitting MotionEvent not supported.", ex);
+ }
+ }
+
+ try {
+ motionEventSplitParams[0] = idBits;
+ Object result = motionEventSplitMethod.invoke(motionEvent, motionEventSplitParams);
+ // MotionEvent.split is guaranteed to return a NonNull result, but the null check is required
+ // for static analysis.
+ if (result != null) {
+ return (MotionEvent) result;
+ } else {
+ return motionEvent;
+ }
+ } catch (InvocationTargetException | IllegalAccessException ex) {
+ throw new RuntimeException("Unable to split MotionEvent.", ex);
+ }
+ }
+
+ private void removePointersFromTouchTargets(int pointerIdBits) {
+ TouchTarget predecessor = null;
+ TouchTarget target = firstHandlingTouchTarget;
+ while (target != null) {
+ TouchTarget next = target.next;
+ if ((target.pointerIdBits & pointerIdBits) != 0) {
+ target.pointerIdBits &= ~pointerIdBits;
+ if (target.pointerIdBits == 0) {
+ if (predecessor == null) {
+ firstHandlingTouchTarget = next;
+ } else {
+ predecessor.next = next;
+ }
+ target = next;
+ continue;
+ }
+ }
+ predecessor = target;
+ target = next;
+ }
+ }
+
+ @Nullable
+ private TouchTarget getTouchTargetForNode(Node node) {
+ for (TouchTarget target = firstHandlingTouchTarget; target != null; target = target.next) {
+ if (target.node == node) {
+ return target;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private Node dispatchTouchEvent(
+ MotionEvent motionEvent,
+ HitTestResult hitTestResult,
+ Node node,
+ int desiredPointerIdBits,
+ boolean bubble) {
+ // Calculate the number of pointers to deliver.
+ int eventPointerIdBits = getPointerIdBits(motionEvent);
+ int finalPointerIdBits = eventPointerIdBits & desiredPointerIdBits;
+
+ // If for some reason we ended up in an inconsistent state where it looks like we
+ // might produce a motion event with no pointers in it, then drop the event.
+ if (finalPointerIdBits == 0) {
+ return null;
+ }
+
+ // Split the motion event if necessary based on the pointer Ids included in the event
+ // compared to the pointer Ids that the node is handling.
+ MotionEvent finalEvent = motionEvent;
+ boolean needsRecycle = false;
+ if (finalPointerIdBits != eventPointerIdBits) {
+ finalEvent = splitMotionEvent(motionEvent, finalPointerIdBits);
+ needsRecycle = true;
+ }
+
+ // Bubble the event up the hierarchy until a node handles the event, or the root is reached.
+ Node resultNode = node;
+ while (resultNode != null) {
+ if (resultNode.dispatchTouchEvent(hitTestResult, finalEvent)) {
+ break;
+ } else {
+ if (bubble) {
+ resultNode = resultNode.getParent();
+ } else {
+ resultNode = null;
+ }
+ }
+ }
+
+ if (resultNode == null) {
+ tryDispatchToSceneTouchListener(hitTestResult, finalEvent);
+ }
+
+ if (needsRecycle) {
+ finalEvent.recycle();
+ }
+
+ return resultNode;
+ }
+
+ private int getPointerIdBits(MotionEvent motionEvent) {
+ int idBits = 0;
+ int pointerCount = motionEvent.getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ idBits |= 1 << motionEvent.getPointerId(i);
+ }
+ return idBits;
+ }
+
+ private TouchTarget addTouchTarget(Node node, int pointerIdBits) {
+ final TouchTarget target = new TouchTarget();
+ target.node = node;
+ target.pointerIdBits = pointerIdBits;
+ target.next = firstHandlingTouchTarget;
+ firstHandlingTouchTarget = target;
+ return target;
+ }
+
+ private void clearTouchTargets() {
+ handlingTouchListener = null;
+ firstHandlingTouchTarget = null;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/ViewTouchHelpers.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/ViewTouchHelpers.java
new file mode 100644
index 0000000..afca542
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/ViewTouchHelpers.java
@@ -0,0 +1,181 @@
+package com.google.ar.sceneform;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.google.ar.sceneform.collision.Plane;
+import com.google.ar.sceneform.collision.Ray;
+import com.google.ar.sceneform.collision.RayHit;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.ViewRenderable;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/** Helper class for utility functions for touching a view rendered in world space. */
+
+class ViewTouchHelpers {
+ /**
+ * Dispatches a touch event to a node's ViewRenderable if that node has a ViewRenderable by
+ * converting the touch event into the local coordinate space of the view.
+ */
+ static boolean dispatchTouchEventToView(Node node, MotionEvent motionEvent) {
+ Preconditions.checkNotNull(node, "Parameter \"node\" was null.");
+ Preconditions.checkNotNull(motionEvent, "Parameter \"motionEvent\" was null.");
+
+ if (!(node.getRenderable() instanceof ViewRenderable)) {
+ return false;
+ }
+
+ if (!node.isActive()) {
+ return false;
+ }
+
+ Scene scene = node.getScene();
+ if (scene == null) {
+ return false;
+ }
+
+ ViewRenderable viewRenderable = (ViewRenderable) node.getRenderable();
+ if (viewRenderable == null) {
+ return false;
+ }
+
+ int pointerCount = motionEvent.getPointerCount();
+
+ MotionEvent.PointerProperties[] pointerProperties =
+ new MotionEvent.PointerProperties[pointerCount];
+
+ MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
+
+ /*
+ * Cast a ray against a plane that extends to infinity located where the view is in 3D space
+ * instead of casting against the node's collision shape. This is important for the UX of touch
+ * events after the initial ACTION_DOWN event. i.e. If a user is dragging a slider and their
+ * finger moves beyond the view the position of their finger relative to the slider should still
+ * be respected.
+ */
+ Plane plane = new Plane(node.getWorldPosition(), node.getForward());
+ RayHit rayHit = new RayHit();
+
+ // Also cast a ray against a back-facing plane because we render the view as double-sided.
+ Plane backPlane = new Plane(node.getWorldPosition(), node.getBack());
+
+ // Convert the pointer coordinates for each pointer into the view's local coordinate space.
+ for (int i = 0; i < pointerCount; i++) {
+ MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
+ MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+
+ motionEvent.getPointerProperties(i, props);
+ motionEvent.getPointerCoords(i, coords);
+
+ Camera camera = scene.getCamera();
+ Ray ray = camera.screenPointToRay(coords.x, coords.y);
+ if (plane.rayIntersection(ray, rayHit)) {
+ Vector3 viewPosition =
+ convertWorldPositionToLocalView(node, rayHit.getPoint(), viewRenderable);
+
+ coords.x = viewPosition.x;
+ coords.y = viewPosition.y;
+ } else if (backPlane.rayIntersection(ray, rayHit)) {
+ Vector3 viewPosition =
+ convertWorldPositionToLocalView(node, rayHit.getPoint(), viewRenderable);
+
+ // Flip the x coordinate for the back-facing plane.
+ coords.x = viewRenderable.getView().getWidth() - viewPosition.x;
+ coords.y = viewPosition.y;
+ } else {
+ coords.clear();
+ props.clear();
+ }
+
+ pointerProperties[i] = props;
+ pointerCoords[i] = coords;
+ }
+
+ // We must copy the touch event with the new coordinates and dispatch it to the view.
+ MotionEvent me =
+ MotionEvent.obtain(
+ motionEvent.getDownTime(),
+ motionEvent.getEventTime(),
+ motionEvent.getAction(),
+ pointerCount,
+ pointerProperties,
+ pointerCoords,
+ motionEvent.getMetaState(),
+ motionEvent.getButtonState(),
+ motionEvent.getXPrecision(),
+ motionEvent.getYPrecision(),
+ motionEvent.getDeviceId(),
+ motionEvent.getEdgeFlags(),
+ motionEvent.getSource(),
+ motionEvent.getFlags());
+
+ return viewRenderable.getView().dispatchTouchEvent(me);
+ }
+
+ static Vector3 convertWorldPositionToLocalView(
+ Node node, Vector3 worldPos, ViewRenderable viewRenderable) {
+ Preconditions.checkNotNull(node, "Parameter \"node\" was null.");
+ Preconditions.checkNotNull(worldPos, "Parameter \"worldPos\" was null.");
+ Preconditions.checkNotNull(viewRenderable, "Parameter \"viewRenderable\" was null.");
+
+ // Find where the view renderable is being touched in local space.
+ // this will be in meters relative to the bottom-middle of the view.
+ Vector3 localPos = node.worldToLocalPoint(worldPos);
+
+ // Calculate the pixels to meters ratio.
+ View view = viewRenderable.getView();
+ int width = view.getWidth();
+ int height = view.getHeight();
+ float pixelsToMetersRatio = getPixelsToMetersRatio(viewRenderable);
+
+ // We must convert the position to pixels
+ int xPixels = (int) (localPos.x * pixelsToMetersRatio);
+ int yPixels = (int) (localPos.y * pixelsToMetersRatio);
+
+ // We must convert the coordinates from the renderable's alignment origin to top-left origin.
+
+ int halfWidth = width / 2;
+ int halfHeight = height / 2;
+
+ ViewRenderable.VerticalAlignment verticalAlignment = viewRenderable.getVerticalAlignment();
+ switch (verticalAlignment) {
+ case BOTTOM:
+ yPixels = height - yPixels;
+ break;
+ case CENTER:
+ yPixels = height - (yPixels + halfHeight);
+ break;
+ case TOP:
+ yPixels = height - (yPixels + height);
+ break;
+ }
+
+ ViewRenderable.HorizontalAlignment horizontalAlignment =
+ viewRenderable.getHorizontalAlignment();
+ switch (horizontalAlignment) {
+ case LEFT:
+ // Do nothing.
+ break;
+ case CENTER:
+ xPixels = (xPixels + halfWidth);
+ break;
+ case RIGHT:
+ xPixels = xPixels + width;
+ break;
+ }
+
+ return new Vector3(xPixels, yPixels, 0.0f);
+ }
+
+ private static float getPixelsToMetersRatio(ViewRenderable viewRenderable) {
+ View view = viewRenderable.getView();
+ int width = view.getWidth();
+ Vector3 size = viewRenderable.getSizer().getSize(viewRenderable.getView());
+
+ if (size.x == 0.0f) {
+ return 0.0f;
+ }
+
+ return (float) width / size.x;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Box.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Box.java
new file mode 100644
index 0000000..cc9bef0
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Box.java
@@ -0,0 +1,304 @@
+package com.google.ar.sceneform.collision;
+
+import android.util.Log;
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.math.MathHelper;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Quaternion;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Mathematical representation of a box. Used to perform intersection and collision tests against
+ * oriented boxes.
+ */
+public class Box extends CollisionShape {
+ private static final String TAG = Box.class.getSimpleName();
+ private final Vector3 center = Vector3.zero();
+ private final Vector3 size = Vector3.one();
+ private final Matrix rotationMatrix = new Matrix();
+
+ /** Create a box with a center of (0,0,0) and a size of (1,1,1). */
+ public Box() {}
+
+ /**
+ * Create a box with a center of (0,0,0) and a specified size.
+ *
+ * @param size the size of the box.
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Box(Vector3 size) {
+ this(size, Vector3.zero());
+ }
+
+ /**
+ * Create a box with a specified center and size.
+ *
+ * @param size the size of the box
+ * @param center the center of the box
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Box(Vector3 size, Vector3 center) {
+ Preconditions.checkNotNull(center, "Parameter \"center\" was null.");
+ Preconditions.checkNotNull(size, "Parameter \"size\" was null.");
+
+ setCenter(center);
+ setSize(size);
+ }
+
+ /**
+ * Set the center of this box.
+ *
+ * @see #getCenter()
+ * @param center the new center of the box
+ */
+ public void setCenter(Vector3 center) {
+ Preconditions.checkNotNull(center, "Parameter \"center\" was null.");
+ this.center.set(center);
+ onChanged();
+ }
+
+ /**
+ * Get a copy of the box's center.
+ *
+ * @see #setCenter(Vector3)
+ * @return a new vector that represents the box's center
+ */
+ public Vector3 getCenter() {
+ return new Vector3(center);
+ }
+
+ /**
+ * Set the size of this box.
+ *
+ * @see #getSize()
+ * @param size the new size of the box
+ */
+ public void setSize(Vector3 size) {
+ Preconditions.checkNotNull(size, "Parameter \"size\" was null.");
+ this.size.set(size);
+ onChanged();
+ }
+
+ /**
+ * Get a copy of the box's size.
+ *
+ * @see #setSize(Vector3)
+ * @return a new vector that represents the box's size
+ */
+ public Vector3 getSize() {
+ return new Vector3(size);
+ }
+
+ /**
+ * Calculate the extents (half the size) of the box.
+ *
+ * @return a new vector that represents the box's extents
+ */
+ public Vector3 getExtents() {
+ return getSize().scaled(0.5f);
+ }
+
+ /**
+ * Set the rotation of this box.
+ *
+ * @see #getRotation()
+ * @param rotation the new rotation of the box
+ */
+ public void setRotation(Quaternion rotation) {
+ Preconditions.checkNotNull(rotation, "Parameter \"rotation\" was null.");
+ rotationMatrix.makeRotation(rotation);
+ onChanged();
+ }
+
+ /**
+ * Get a copy of the box's rotation.
+ *
+ * @see #setRotation(Quaternion)
+ * @return a new quaternion that represents the box's rotation
+ */
+ public Quaternion getRotation() {
+ Quaternion result = new Quaternion();
+ rotationMatrix.extractQuaternion(result);
+ return result;
+ }
+
+ @Override
+ public Box makeCopy() {
+ return new Box(getSize(), getCenter());
+ }
+
+ /**
+ * Get the raw rotation matrix representing the box's orientation. Do not modify directly.
+ * Instead, use setRotation.
+ *
+ * @return a reference to the box's raw rotation matrix
+ */
+ Matrix getRawRotationMatrix() {
+ return rotationMatrix;
+ }
+
+ /** @hide protected method */
+ @Override
+ protected boolean rayIntersection(Ray ray, RayHit result) {
+ Preconditions.checkNotNull(ray, "Parameter \"ray\" was null.");
+ Preconditions.checkNotNull(result, "Parameter \"result\" was null.");
+
+ Vector3 rayDirection = ray.getDirection();
+ Vector3 rayOrigin = ray.getOrigin();
+ Vector3 max = getExtents();
+ Vector3 min = max.negated();
+
+ // tMin is the farthest "near" intersection (amongst the X,Y and Z planes pairs)
+ float tMin = Float.MIN_VALUE;
+
+ // tMax is the nearest "far" intersection (amongst the X,Y and Z planes pairs)
+ float tMax = Float.MAX_VALUE;
+
+ Vector3 delta = Vector3.subtract(center, rayOrigin);
+
+ // Test intersection with the 2 planes perpendicular to the OBB's x axis.
+ float[] axes = rotationMatrix.data;
+ Vector3 axis = new Vector3(axes[0], axes[1], axes[2]);
+ float e = Vector3.dot(axis, delta);
+ float f = Vector3.dot(rayDirection, axis);
+
+ if (!MathHelper.almostEqualRelativeAndAbs(f, 0.0f)) {
+ float t1 = (e + min.x) / f;
+ float t2 = (e + max.x) / f;
+
+ if (t1 > t2) {
+ float temp = t1;
+ t1 = t2;
+ t2 = temp;
+ }
+
+ tMax = Math.min(t2, tMax);
+ tMin = Math.max(t1, tMin);
+
+ if (tMax < tMin) {
+ return false;
+ }
+ } else if (-e + min.x > 0.0f || -e + max.x < 0.0f) {
+ // Ray is almost parallel to one of the planes.
+ return false;
+ }
+
+ // Test intersection with the 2 planes perpendicular to the OBB's y axis.
+ axis = new Vector3(axes[4], axes[5], axes[6]);
+ e = Vector3.dot(axis, delta);
+ f = Vector3.dot(rayDirection, axis);
+
+ if (!MathHelper.almostEqualRelativeAndAbs(f, 0.0f)) {
+ float t1 = (e + min.y) / f;
+ float t2 = (e + max.y) / f;
+
+ if (t1 > t2) {
+ float temp = t1;
+ t1 = t2;
+ t2 = temp;
+ }
+
+ tMax = Math.min(t2, tMax);
+ tMin = Math.max(t1, tMin);
+
+ if (tMax < tMin) {
+ return false;
+ }
+ } else if (-e + min.y > 0.0f || -e + max.y < 0.0f) {
+ // Ray is almost parallel to one of the planes.
+ return false;
+ }
+
+ // Test intersection with the 2 planes perpendicular to the OBB's z axis.
+ axis = new Vector3(axes[8], axes[9], axes[10]);
+ e = Vector3.dot(axis, delta);
+ f = Vector3.dot(rayDirection, axis);
+
+ if (!MathHelper.almostEqualRelativeAndAbs(f, 0.0f)) {
+ float t1 = (e + min.z) / f;
+ float t2 = (e + max.z) / f;
+
+ if (t1 > t2) {
+ float temp = t1;
+ t1 = t2;
+ t2 = temp;
+ }
+
+ tMax = Math.min(t2, tMax);
+ tMin = Math.max(t1, tMin);
+
+ if (tMax < tMin) {
+ return false;
+ }
+ } else if (-e + min.z > 0.0f || -e + max.z < 0.0f) {
+ // Ray is almost parallel to one of the planes.
+ return false;
+ }
+
+ result.setDistance(tMin);
+ result.setPoint(ray.getPoint(result.getDistance()));
+ return true;
+ }
+
+ /** @hide protected method */
+ @Override
+ protected boolean shapeIntersection(CollisionShape shape) {
+ Preconditions.checkNotNull(shape, "Parameter \"shape\" was null.");
+ return shape.boxIntersection(this);
+ }
+
+ /** @hide protected method */
+ @Override
+ protected boolean sphereIntersection(Sphere sphere) {
+ return Intersections.sphereBoxIntersection(sphere, this);
+ }
+
+ /** @hide protected method */
+ @Override
+ protected boolean boxIntersection(Box box) {
+ return Intersections.boxBoxIntersection(this, box);
+ }
+
+ @Override
+ CollisionShape transform(TransformProvider transformProvider) {
+ Preconditions.checkNotNull(transformProvider, "Parameter \"transformProvider\" was null.");
+
+ Box result = new Box();
+ transform(transformProvider, result);
+ return result;
+ }
+
+ @Override
+ void transform(TransformProvider transformProvider, CollisionShape result) {
+ Preconditions.checkNotNull(transformProvider, "Parameter \"transformProvider\" was null.");
+ Preconditions.checkNotNull(result, "Parameter \"result\" was null.");
+
+ if (!(result instanceof Box)) {
+ Log.w(TAG, "Cannot pass CollisionShape of a type other than Box into Box.transform.");
+ return;
+ }
+
+ if (result == this) {
+ throw new IllegalArgumentException("Box cannot transform itself.");
+ }
+
+ Box resultBox = (Box) result;
+
+ Matrix modelMatrix = transformProvider.getWorldModelMatrix();
+
+ // Transform the center of the box.
+ resultBox.center.set(modelMatrix.transformPoint(center));
+
+ // Transform the size of the box.
+ Vector3 worldScale = new Vector3();
+ modelMatrix.decomposeScale(worldScale);
+ resultBox.size.x = size.x * worldScale.x;
+ resultBox.size.y = size.y * worldScale.y;
+ resultBox.size.z = size.z * worldScale.z;
+
+ // Transform the rotation of the box.
+ modelMatrix.decomposeRotation(worldScale, resultBox.rotationMatrix);
+ Matrix.multiply(rotationMatrix, resultBox.rotationMatrix, resultBox.rotationMatrix);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Collider.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Collider.java
new file mode 100644
index 0000000..f87b18e
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Collider.java
@@ -0,0 +1,99 @@
+package com.google.ar.sceneform.collision;
+
+import androidx.annotation.Nullable;
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.utilities.ChangeId;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Represents the collision information associated with a transformation that can be attached to the
+ * collision system. Not publicly exposed.
+ *
+ * @hide
+ */
+public class Collider {
+ private TransformProvider transformProvider;
+ @Nullable private CollisionSystem attachedCollisionSystem;
+
+ private CollisionShape localShape;
+ @Nullable private CollisionShape cachedWorldShape;
+
+ private boolean isWorldShapeDirty;
+ private int shapeId = ChangeId.EMPTY_ID;
+
+ /** @hide */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Collider(TransformProvider transformProvider, CollisionShape localCollisionShape) {
+ Preconditions.checkNotNull(transformProvider, "Parameter \"transformProvider\" was null.");
+ Preconditions.checkNotNull(localCollisionShape, "Parameter \"localCollisionShape\" was null.");
+
+ this.transformProvider = transformProvider;
+ setShape(localCollisionShape);
+ }
+
+ /** @hide */
+ public void setShape(CollisionShape localCollisionShape) {
+ Preconditions.checkNotNull(localCollisionShape, "Parameter \"localCollisionShape\" was null.");
+
+ localShape = localCollisionShape;
+ cachedWorldShape = null;
+ }
+
+ /** @hide */
+ public CollisionShape getShape() {
+ return localShape;
+ }
+
+ public TransformProvider getTransformProvider() {
+ return transformProvider;
+ }
+
+ /** @hide */
+ @Nullable
+ public CollisionShape getTransformedShape() {
+ updateCachedWorldShape();
+ return cachedWorldShape;
+ }
+
+ /** @hide */
+ public void setAttachedCollisionSystem(@Nullable CollisionSystem collisionSystem) {
+ if (attachedCollisionSystem != null) {
+ attachedCollisionSystem.removeCollider(this);
+ }
+
+ attachedCollisionSystem = collisionSystem;
+
+ if (attachedCollisionSystem != null) {
+ attachedCollisionSystem.addCollider(this);
+ }
+ }
+
+ /** @hide */
+ public void markWorldShapeDirty() {
+ isWorldShapeDirty = true;
+ }
+
+ private boolean doesCachedWorldShapeNeedUpdate() {
+ if (localShape == null) {
+ return false;
+ }
+
+ ChangeId changeId = localShape.getId();
+ return changeId.checkChanged(shapeId) || isWorldShapeDirty || cachedWorldShape == null;
+ }
+
+ private void updateCachedWorldShape() {
+ if (!doesCachedWorldShapeNeedUpdate()) {
+ return;
+ }
+
+ if (cachedWorldShape == null) {
+ cachedWorldShape = localShape.transform(transformProvider);
+ } else {
+ localShape.transform(transformProvider, cachedWorldShape);
+ }
+
+ ChangeId changeId = localShape.getId();
+ shapeId = changeId.get();
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/CollisionShape.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/CollisionShape.java
new file mode 100644
index 0000000..1db049d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/CollisionShape.java
@@ -0,0 +1,45 @@
+package com.google.ar.sceneform.collision;
+
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.utilities.ChangeId;
+
+/** Base class for all types of shapes that collision checks can be performed against. */
+public abstract class CollisionShape {
+ private final ChangeId changeId = new ChangeId();
+
+ public abstract CollisionShape makeCopy();
+
+ /**
+ * Must be called by subclasses when the shape changes to inform listeners of the change.
+ *
+ * @hide
+ */
+ protected void onChanged() {
+ changeId.update();
+ }
+
+ /** @hide */
+ protected abstract boolean rayIntersection(Ray ray, RayHit result);
+
+ /** @hide */
+ protected abstract boolean shapeIntersection(CollisionShape shape);
+
+ /** @hide */
+ protected abstract boolean sphereIntersection(Sphere sphere);
+
+ /** @hide */
+ protected abstract boolean boxIntersection(Box box);
+
+ @SuppressWarnings("initialization")
+ CollisionShape() {
+ changeId.update();
+ }
+
+ ChangeId getId() {
+ return changeId;
+ }
+
+ abstract CollisionShape transform(TransformProvider transformProvider);
+
+ abstract void transform(TransformProvider transformProvider, CollisionShape result);
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/CollisionSystem.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/CollisionSystem.java
new file mode 100644
index 0000000..5985561
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/CollisionSystem.java
@@ -0,0 +1,159 @@
+package com.google.ar.sceneform.collision;
+
+import androidx.annotation.Nullable;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Manages all of the colliders within a scene.
+ *
+ * @hide
+ */
+public class CollisionSystem {
+ private static final String TAG = CollisionSystem.class.getSimpleName();
+
+ // TODO: Store things in some spatial partition or another.
+ private final ArrayList colliders = new ArrayList<>();
+
+ public void addCollider(Collider collider) {
+ Preconditions.checkNotNull(collider, "Parameter \"collider\" was null.");
+ colliders.add(collider);
+ }
+
+ public void removeCollider(Collider collider) {
+ Preconditions.checkNotNull(collider, "Parameter \"collider\" was null.");
+ colliders.remove(collider);
+ }
+
+ @Nullable
+ public Collider raycast(Ray ray, RayHit resultHit) {
+ Preconditions.checkNotNull(ray, "Parameter \"ray\" was null.");
+ Preconditions.checkNotNull(resultHit, "Parameter \"resultHit\" was null.");
+
+ resultHit.reset();
+ Collider result = null;
+ RayHit tempResult = new RayHit();
+ for (Collider collider : colliders) {
+ CollisionShape collisionShape = collider.getTransformedShape();
+ if (collisionShape == null) {
+ continue;
+ }
+
+ if (collisionShape.rayIntersection(ray, tempResult)) {
+ if (tempResult.getDistance() < resultHit.getDistance()) {
+ resultHit.set(tempResult);
+ result = collider;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @SuppressWarnings("AndroidApiChecker")
+ public int raycastAll(
+ Ray ray,
+ ArrayList resultBuffer,
+ @Nullable BiConsumer processResult,
+ Supplier allocateResult) {
+ Preconditions.checkNotNull(ray, "Parameter \"ray\" was null.");
+ Preconditions.checkNotNull(resultBuffer, "Parameter \"resultBuffer\" was null.");
+ Preconditions.checkNotNull(allocateResult, "Parameter \"allocateResult\" was null.");
+
+ RayHit tempResult = new RayHit();
+ int hitCount = 0;
+
+ // Check the ray against all the colliders.
+ for (Collider collider : colliders) {
+ CollisionShape collisionShape = collider.getTransformedShape();
+ if (collisionShape == null) {
+ continue;
+ }
+
+ if (collisionShape.rayIntersection(ray, tempResult)) {
+ hitCount++;
+ T result = null;
+ if (resultBuffer.size() >= hitCount) {
+ result = resultBuffer.get(hitCount - 1);
+ } else {
+ result = allocateResult.get();
+ resultBuffer.add(result);
+ }
+
+ result.reset();
+ result.set(tempResult);
+
+ if (processResult != null) {
+ processResult.accept(result, collider);
+ }
+ }
+ }
+
+ // Reset extra hits in the buffer.
+ for (int i = hitCount; i < resultBuffer.size(); i++) {
+ resultBuffer.get(i).reset();
+ }
+
+ // Sort the hits by distance.
+ Collections.sort(resultBuffer, (a, b) -> Float.compare(a.getDistance(), b.getDistance()));
+
+ return hitCount;
+ }
+
+ @Nullable
+ public Collider intersects(Collider collider) {
+ Preconditions.checkNotNull(collider, "Parameter \"collider\" was null.");
+
+ CollisionShape collisionShape = collider.getTransformedShape();
+ if (collisionShape == null) {
+ return null;
+ }
+
+ for (Collider otherCollider : colliders) {
+ if (otherCollider == collider) {
+ continue;
+ }
+
+ CollisionShape otherCollisionShape = otherCollider.getTransformedShape();
+ if (otherCollisionShape == null) {
+ continue;
+ }
+
+ if (collisionShape.shapeIntersection(otherCollisionShape)) {
+ return otherCollider;
+ }
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("AndroidApiChecker")
+ public void intersectsAll(Collider collider, Consumer processResult) {
+ Preconditions.checkNotNull(collider, "Parameter \"collider\" was null.");
+ Preconditions.checkNotNull(processResult, "Parameter \"processResult\" was null.");
+
+ CollisionShape collisionShape = collider.getTransformedShape();
+ if (collisionShape == null) {
+ return;
+ }
+
+ for (Collider otherCollider : colliders) {
+ if (otherCollider == collider) {
+ continue;
+ }
+
+ CollisionShape otherCollisionShape = otherCollider.getTransformedShape();
+ if (otherCollisionShape == null) {
+ continue;
+ }
+
+ if (collisionShape.shapeIntersection(otherCollisionShape)) {
+ processResult.accept(otherCollider);
+ }
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Intersections.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Intersections.java
new file mode 100644
index 0000000..d1b6c57
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Intersections.java
@@ -0,0 +1,208 @@
+package com.google.ar.sceneform.collision;
+
+import static com.google.ar.sceneform.math.Vector3.add;
+import static com.google.ar.sceneform.math.Vector3.subtract;
+
+import com.google.ar.sceneform.math.MathHelper;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Implementation of common intersection tests used for collision detection. */
+class Intersections {
+ private static final int NUM_VERTICES_PER_BOX = 8;
+ private static final int NUM_TEST_AXES = 15;
+
+ /** Determine if two spheres intersect with each other. */
+ static boolean sphereSphereIntersection(Sphere sphere1, Sphere sphere2) {
+ Preconditions.checkNotNull(sphere1, "Parameter \"sphere1\" was null.");
+ Preconditions.checkNotNull(sphere2, "Parameter \"sphere2\" was null.");
+
+ float combinedRadius = sphere1.getRadius() + sphere2.getRadius();
+ float combinedRadiusSquared = combinedRadius * combinedRadius;
+ Vector3 difference = Vector3.subtract(sphere2.getCenter(), sphere1.getCenter());
+ float differenceLengthSquared = Vector3.dot(difference, difference);
+
+ return differenceLengthSquared - combinedRadiusSquared <= 0.0f
+ && differenceLengthSquared != 0.0f;
+ }
+
+ /** Determine if two boxes intersect with each other. */
+ static boolean boxBoxIntersection(Box box1, Box box2) {
+ Preconditions.checkNotNull(box1, "Parameter \"box1\" was null.");
+ Preconditions.checkNotNull(box2, "Parameter \"box2\" was null.");
+
+ // Get the vertices of the boxes.
+ List box1Vertices = getVerticesFromBox(box1);
+ List box2Vertices = getVerticesFromBox(box2);
+
+ // Determine the test axes
+ Matrix box1Rotation = box1.getRawRotationMatrix();
+ Matrix box2Rotation = box2.getRawRotationMatrix();
+ ArrayList testAxes = new ArrayList<>(NUM_TEST_AXES);
+ testAxes.add(extractXAxisFromRotationMatrix(box1Rotation));
+ testAxes.add(extractYAxisFromRotationMatrix(box1Rotation));
+ testAxes.add(extractZAxisFromRotationMatrix(box1Rotation));
+ testAxes.add(extractXAxisFromRotationMatrix(box2Rotation));
+ testAxes.add(extractYAxisFromRotationMatrix(box2Rotation));
+ testAxes.add(extractZAxisFromRotationMatrix(box2Rotation));
+
+ for (int i = 0; i < 3; i++) {
+ testAxes.add(Vector3.cross(testAxes.get(i), testAxes.get(0)));
+ testAxes.add(Vector3.cross(testAxes.get(i), testAxes.get(1)));
+ testAxes.add(Vector3.cross(testAxes.get(i), testAxes.get(2)));
+ }
+
+ // Attempt to find a separating axis.
+ for (int i = 0; i < testAxes.size(); i++) {
+ if (!testSeparatingAxis(box1Vertices, box2Vertices, testAxes.get(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /** Determine if a sphere and a box intersect with each other. */
+ static boolean sphereBoxIntersection(Sphere sphere, Box box) {
+ Preconditions.checkNotNull(sphere, "Parameter \"sphere\" was null.");
+ Preconditions.checkNotNull(box, "Parameter \"box\" was null.");
+
+ Vector3 point = closestPointOnBox(sphere.getCenter(), box);
+ Vector3 sphereDiff = Vector3.subtract(point, sphere.getCenter());
+ float sphereDiffLengthSquared = Vector3.dot(sphereDiff, sphereDiff);
+
+ if (sphereDiffLengthSquared > sphere.getRadius() * sphere.getRadius()) {
+ return false;
+ }
+
+ if (MathHelper.almostEqualRelativeAndAbs(sphereDiffLengthSquared, 0.0f)) {
+ Vector3 boxDiff = Vector3.subtract(point, box.getCenter());
+ float boxDiffLengthSquared = Vector3.dot(boxDiff, boxDiff);
+ if (MathHelper.almostEqualRelativeAndAbs(boxDiffLengthSquared, 0.0f)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static Vector3 closestPointOnBox(Vector3 point, Box box) {
+ Vector3 result = new Vector3(box.getCenter());
+ Vector3 diff = Vector3.subtract(point, box.getCenter());
+ Matrix boxRotation = box.getRawRotationMatrix();
+ Vector3 boxExtents = box.getExtents();
+
+ // x-axis
+ {
+ Vector3 axis = extractXAxisFromRotationMatrix(boxRotation);
+ float distance = Vector3.dot(diff, axis);
+
+ if (distance > boxExtents.x) {
+ distance = boxExtents.x;
+ } else if (distance < -boxExtents.x) {
+ distance = -boxExtents.x;
+ }
+
+ result = Vector3.add(result, axis.scaled(distance));
+ }
+
+ // y-axis
+ {
+ Vector3 axis = extractYAxisFromRotationMatrix(boxRotation);
+ float distance = Vector3.dot(diff, axis);
+
+ if (distance > boxExtents.y) {
+ distance = boxExtents.y;
+ } else if (distance < -boxExtents.y) {
+ distance = -boxExtents.y;
+ }
+
+ result = Vector3.add(result, axis.scaled(distance));
+ }
+
+ // z-axis
+ {
+ Vector3 axis = extractZAxisFromRotationMatrix(boxRotation);
+ float distance = Vector3.dot(diff, axis);
+
+ if (distance > boxExtents.z) {
+ distance = boxExtents.z;
+ } else if (distance < -boxExtents.z) {
+ distance = -boxExtents.z;
+ }
+
+ result = Vector3.add(result, axis.scaled(distance));
+ }
+
+ return result;
+ }
+
+ private static boolean testSeparatingAxis(
+ List vertices1, List vertices2, Vector3 axis) {
+ float min1 = Float.MAX_VALUE;
+ float max1 = Float.MIN_VALUE;
+ for (int i = 0; i < vertices1.size(); ++i) {
+ float projection = Vector3.dot(axis, vertices1.get(i));
+ min1 = Math.min(projection, min1);
+ max1 = Math.max(projection, max1);
+ }
+
+ float min2 = Float.MAX_VALUE;
+ float max2 = Float.MIN_VALUE;
+ for (int i = 0; i < vertices2.size(); i++) {
+ float projection = Vector3.dot(axis, vertices2.get(i));
+ min2 = Math.min(projection, min2);
+ max2 = Math.max(projection, max2);
+ }
+
+ return min2 <= max1 && min1 <= max2;
+ }
+
+ /** Converts a box into an array of 8 vertices that represent the corners of the box. */
+ private static List getVerticesFromBox(Box box) {
+ Preconditions.checkNotNull(box, "Parameter \"box\" was null.");
+
+ // Get the properties of the box.
+ Vector3 center = box.getCenter();
+ Vector3 extents = box.getExtents();
+ Matrix rotation = box.getRawRotationMatrix();
+
+ // Get the rotation axes of the box.
+ Vector3 xAxis = extractXAxisFromRotationMatrix(rotation);
+ Vector3 yAxis = extractYAxisFromRotationMatrix(rotation);
+ Vector3 zAxis = extractZAxisFromRotationMatrix(rotation);
+
+ // Scale the rotation axes by the extents.
+ Vector3 xScaled = xAxis.scaled(extents.x);
+ Vector3 yScaled = yAxis.scaled(extents.y);
+ Vector3 zScaled = zAxis.scaled(extents.z);
+
+ // Calculate the 8 vertices of the box.
+ ArrayList vertices = new ArrayList<>(NUM_VERTICES_PER_BOX);
+ vertices.add(add(add(add(center, xScaled), yScaled), zScaled));
+ vertices.add(add(add(subtract(center, xScaled), yScaled), zScaled));
+ vertices.add(add(subtract(add(center, xScaled), yScaled), zScaled));
+ vertices.add(subtract(add(add(center, xScaled), yScaled), zScaled));
+ vertices.add(subtract(subtract(subtract(center, xScaled), yScaled), zScaled));
+ vertices.add(subtract(subtract(add(center, xScaled), yScaled), zScaled));
+ vertices.add(subtract(add(subtract(center, xScaled), yScaled), zScaled));
+ vertices.add(add(subtract(subtract(center, xScaled), yScaled), zScaled));
+
+ return vertices;
+ }
+
+ private static Vector3 extractXAxisFromRotationMatrix(Matrix matrix) {
+ return new Vector3(matrix.data[0], matrix.data[4], matrix.data[8]);
+ }
+
+ private static Vector3 extractYAxisFromRotationMatrix(Matrix matrix) {
+ return new Vector3(matrix.data[1], matrix.data[5], matrix.data[9]);
+ }
+
+ private static Vector3 extractZAxisFromRotationMatrix(Matrix matrix) {
+ return new Vector3(matrix.data[2], matrix.data[6], matrix.data[10]);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Plane.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Plane.java
new file mode 100644
index 0000000..46c33e5
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Plane.java
@@ -0,0 +1,63 @@
+package com.google.ar.sceneform.collision;
+
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Mathematical representation of a plane with an infinite size. Used for intersection tests.
+ *
+ * @hide
+ */
+public class Plane {
+ private final Vector3 center = new Vector3();
+ private final Vector3 normal = new Vector3();
+
+ private static final double NEAR_ZERO_THRESHOLD = 1e-6;
+
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Plane(Vector3 center, Vector3 normal) {
+ setCenter(center);
+ setNormal(normal);
+ }
+
+ public void setCenter(Vector3 center) {
+ Preconditions.checkNotNull(center, "Parameter \"center\" was null.");
+
+ this.center.set(center);
+ }
+
+ public Vector3 getCenter() {
+ return new Vector3(center);
+ }
+
+ public void setNormal(Vector3 normal) {
+ Preconditions.checkNotNull(normal, "Parameter \"normal\" was null.");
+ this.normal.set(normal.normalized());
+ }
+
+ public Vector3 getNormal() {
+ return new Vector3(normal);
+ }
+
+ public boolean rayIntersection(Ray ray, RayHit result) {
+ Preconditions.checkNotNull(ray, "Parameter \"ray\" was null.");
+ Preconditions.checkNotNull(result, "Parameter \"result\" was null.");
+
+ Vector3 rayDirection = ray.getDirection();
+ Vector3 rayOrigin = ray.getOrigin();
+
+ float denominator = Vector3.dot(normal, rayDirection);
+ if (Math.abs(denominator) > NEAR_ZERO_THRESHOLD) {
+ Vector3 delta = Vector3.subtract(center, rayOrigin);
+ float distance = Vector3.dot(delta, normal) / denominator;
+ if (distance >= 0) {
+ result.setDistance(distance);
+ result.setPoint(ray.getPoint(result.getDistance()));
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Ray.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Ray.java
new file mode 100644
index 0000000..54da552
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Ray.java
@@ -0,0 +1,83 @@
+package com.google.ar.sceneform.collision;
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/** Mathematical representation of a ray. Used to perform intersection and collision tests. */
+public class Ray {
+ private Vector3 origin = new Vector3();
+ private Vector3 direction = Vector3.forward();
+
+ /** Create a ray with an origin of (0,0,0) and a direction of Vector3.forward(). */
+ public Ray() {}
+
+ /**
+ * Create a ray with a specified origin and direction. The direction will automatically be
+ * normalized.
+ *
+ * @param origin the ray's origin
+ * @param direction the ray's direction
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Ray(Vector3 origin, Vector3 direction) {
+ Preconditions.checkNotNull(origin, "Parameter \"origin\" was null.");
+ Preconditions.checkNotNull(direction, "Parameter \"direction\" was null.");
+
+ setOrigin(origin);
+ setDirection(direction);
+ }
+
+ /**
+ * Set the origin of the ray in world coordinates.
+ *
+ * @param origin the new origin of the ray.
+ */
+ public void setOrigin(Vector3 origin) {
+ Preconditions.checkNotNull(origin, "Parameter \"origin\" was null.");
+ this.origin.set(origin);
+ }
+
+ /**
+ * Get the origin of the ray.
+ *
+ * @return a new vector that represents the ray's origin
+ */
+ public Vector3 getOrigin() {
+ return new Vector3(origin);
+ }
+
+ /**
+ * Set the direction of the ray. The direction will automatically be normalized.
+ *
+ * @param direction the new direction of the ray
+ */
+ public void setDirection(Vector3 direction) {
+ Preconditions.checkNotNull(direction, "Parameter \"direction\" was null.");
+
+ this.direction.set(direction.normalized());
+ }
+
+ /**
+ * Get the direction of the ray.
+ *
+ * @return a new vector that represents the ray's direction
+ */
+ public Vector3 getDirection() {
+ return new Vector3(direction);
+ }
+
+ /**
+ * Get a point at a distance along the ray.
+ *
+ * @param distance distance along the ray of the point
+ * @return a new vector that represents a point at a distance along the ray.
+ */
+ public Vector3 getPoint(float distance) {
+ return Vector3.add(origin, direction.scaled(distance));
+ }
+
+ @Override
+ public String toString() {
+ return "[Origin:" + origin + ", Direction:" + direction + "]";
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/RayHit.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/RayHit.java
new file mode 100644
index 0000000..555c0bc
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/RayHit.java
@@ -0,0 +1,57 @@
+package com.google.ar.sceneform.collision;
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Stores the results of ray intersection tests against various types of CollisionShape.
+ *
+ * @hide
+ */
+public class RayHit {
+ private float distance = Float.MAX_VALUE;
+ private final Vector3 point = new Vector3();
+
+ /** @hide */
+ public void setDistance(float distance) {
+ this.distance = distance;
+ }
+
+ /**
+ * Get the distance along the ray to the impact point on the surface of the collision shape.
+ *
+ * @return distance along the ray that the hit occurred at
+ */
+ public float getDistance() {
+ return distance;
+ }
+
+ /** @hide */
+ public void setPoint(Vector3 point) {
+ Preconditions.checkNotNull(point, "Parameter \"point\" was null.");
+ this.point.set(point);
+ }
+
+ /**
+ * Get the position in world-space where the ray hit the collision shape.
+ *
+ * @return a new vector that represents the position in world-space that the hit occurred at
+ */
+ public Vector3 getPoint() {
+ return new Vector3(point);
+ }
+
+ /** @hide */
+ public void set(RayHit other) {
+ Preconditions.checkNotNull(other, "Parameter \"other\" was null.");
+
+ setDistance(other.distance);
+ setPoint(other.point);
+ }
+
+ /** @hide */
+ public void reset() {
+ distance = Float.MAX_VALUE;
+ point.set(0, 0, 0);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Sphere.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Sphere.java
new file mode 100644
index 0000000..f8fb83a
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/collision/Sphere.java
@@ -0,0 +1,185 @@
+package com.google.ar.sceneform.collision;
+
+import android.util.Log;
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Mathematical representation of a sphere. Used to perform intersection and collision tests against
+ * spheres.
+ */
+public class Sphere extends CollisionShape {
+ private static final String TAG = Sphere.class.getSimpleName();
+
+ private final Vector3 center = new Vector3();
+ private float radius = 1.0f;
+
+ /** Create a sphere with a center of (0,0,0) and a radius of 1. */
+ public Sphere() {}
+
+ /**
+ * Create a sphere with a center of (0,0,0) and a specified radius.
+ *
+ * @param radius the radius of the sphere
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Sphere(float radius) {
+ this(radius, Vector3.zero());
+ }
+
+ /**
+ * Create a sphere with a specified center and radius.
+ *
+ * @param radius the radius of the sphere
+ * @param center the center of the sphere
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Sphere(float radius, Vector3 center) {
+ Preconditions.checkNotNull(center, "Parameter \"center\" was null.");
+
+ setCenter(center);
+ setRadius(radius);
+ }
+
+ /**
+ * Set the center of this sphere.
+ *
+ * @see #getCenter()
+ * @param center the new center of the sphere
+ */
+ public void setCenter(Vector3 center) {
+ Preconditions.checkNotNull(center, "Parameter \"center\" was null.");
+ this.center.set(center);
+ onChanged();
+ }
+
+ /**
+ * Get a copy of the sphere's center.
+ *
+ * @see #setCenter(Vector3)
+ * @return a new vector that represents the sphere's center
+ */
+ public Vector3 getCenter() {
+ return new Vector3(center);
+ }
+
+ /**
+ * Set the radius of the sphere.
+ *
+ * @see #getRadius()
+ * @param radius the new radius of the sphere
+ */
+ public void setRadius(float radius) {
+ this.radius = radius;
+ onChanged();
+ }
+
+ /**
+ * Get the radius of the sphere.
+ *
+ * @see #setRadius(float)
+ * @return the radius of the sphere
+ */
+ public float getRadius() {
+ return radius;
+ }
+
+ @Override
+ public Sphere makeCopy() {
+ return new Sphere(getRadius(), getCenter());
+ }
+
+ /** @hide */
+ @Override
+ protected boolean rayIntersection(Ray ray, RayHit result) {
+ Preconditions.checkNotNull(ray, "Parameter \"ray\" was null.");
+ Preconditions.checkNotNull(result, "Parameter \"result\" was null.");
+
+ Vector3 rayDirection = ray.getDirection();
+ Vector3 rayOrigin = ray.getOrigin();
+
+ Vector3 difference = Vector3.subtract(rayOrigin, center);
+ float b = 2.0f * Vector3.dot(difference, rayDirection);
+ float c = Vector3.dot(difference, difference) - radius * radius;
+ float discriminant = b * b - 4.0f * c;
+
+ if (discriminant < 0.0f) {
+ return false;
+ }
+
+ float discriminantSqrt = (float) Math.sqrt(discriminant);
+ float tMinus = (-b - discriminantSqrt) / 2.0f;
+ float tPlus = (-b + discriminantSqrt) / 2.0f;
+
+ if (tMinus < 0.0f && tPlus < 0.0f) {
+ return false;
+ }
+
+ if (tMinus < 0 && tPlus > 0) {
+ result.setDistance(tPlus);
+ } else {
+ result.setDistance(tMinus);
+ }
+
+ result.setPoint(ray.getPoint(result.getDistance()));
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ protected boolean shapeIntersection(CollisionShape shape) {
+ Preconditions.checkNotNull(shape, "Parameter \"shape\" was null.");
+ return shape.sphereIntersection(this);
+ }
+
+ /** @hide */
+ @Override
+ protected boolean sphereIntersection(Sphere sphere) {
+ return Intersections.sphereSphereIntersection(this, sphere);
+ }
+
+ /** @hide */
+ @Override
+ protected boolean boxIntersection(Box box) {
+ return Intersections.sphereBoxIntersection(this, box);
+ }
+
+ @Override
+ CollisionShape transform(TransformProvider transformProvider) {
+ Preconditions.checkNotNull(transformProvider, "Parameter \"transformProvider\" was null.");
+
+ Sphere result = new Sphere();
+ transform(transformProvider, result);
+ return result;
+ }
+
+ @Override
+ void transform(TransformProvider transformProvider, CollisionShape result) {
+ Preconditions.checkNotNull(transformProvider, "Parameter \"transformProvider\" was null.");
+ Preconditions.checkNotNull(result, "Parameter \"result\" was null.");
+
+ if (!(result instanceof Sphere)) {
+ Log.w(TAG, "Cannot pass CollisionShape of a type other than Sphere into Sphere.transform.");
+ return;
+ }
+
+ Sphere resultSphere = (Sphere) result;
+
+ Matrix modelMatrix = transformProvider.getWorldModelMatrix();
+
+ // Transform the center of the sphere.
+ resultSphere.setCenter(modelMatrix.transformPoint(center));
+
+ // Transform the radius of the sphere.
+ Vector3 worldScale = new Vector3();
+ modelMatrix.decomposeScale(worldScale);
+ // Find the max component scale, ignoring sign.
+ float maxScale =
+ Math.max(
+ Math.abs(Math.min(Math.min(worldScale.x, worldScale.y), worldScale.z)),
+ Math.max(Math.max(worldScale.x, worldScale.y), worldScale.z));
+ resultSphere.radius = radius * maxScale;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/common/TransformProvider.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/common/TransformProvider.java
new file mode 100644
index 0000000..e879123
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/common/TransformProvider.java
@@ -0,0 +1,13 @@
+package com.google.ar.sceneform.common;
+
+import com.google.ar.sceneform.math.Matrix;
+
+/**
+ * Interface for providing information about a 3D transformation. See {@link
+ * com.google.ar.sceneform.Node}.
+ *
+ * @hide
+ */
+public interface TransformProvider {
+ Matrix getWorldModelMatrix();
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/AabbDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/AabbDef.java
new file mode 100644
index 0000000..a04fdcd
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/AabbDef.java
@@ -0,0 +1,33 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class AabbDef extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public AabbDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public Vec3 min() { return min(new Vec3()); }
+ public Vec3 min(Vec3 obj) { return obj.__assign(bb_pos + 0, bb); }
+ public Vec3 max() { return max(new Vec3()); }
+ public Vec3 max(Vec3 obj) { return obj.__assign(bb_pos + 12, bb); }
+
+ public static int createAabbDef(FlatBufferBuilder builder, float min_x, float min_y, float min_z, float max_x, float max_y, float max_z) {
+ builder.prep(4, 24);
+ builder.prep(4, 12);
+ builder.putFloat(max_z);
+ builder.putFloat(max_y);
+ builder.putFloat(max_x);
+ builder.prep(4, 12);
+ builder.putFloat(min_z);
+ builder.putFloat(min_y);
+ builder.putFloat(min_x);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ArcDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ArcDef.java
new file mode 100644
index 0000000..3c01bc3
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ArcDef.java
@@ -0,0 +1,50 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * ArcDef defines a portion of a circulur annulus.
+ */
+public final class ArcDef extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public ArcDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The angle (in radians) in which the start of the arc should be poised at.
+ * 0 = vertical [0,1]. PI = [0,-1], PI/2 = [1,0].
+ */
+ public float startAngle() { return bb.getFloat(bb_pos + 0); }
+ /**
+ * Size of the arc measured in radians. PI = half circle, 2 PI = full circle.
+ */
+ public float angleSize() { return bb.getFloat(bb_pos + 4); }
+ /**
+ * Inner radius of the arc.
+ */
+ public float innerRadius() { return bb.getFloat(bb_pos + 8); }
+ /**
+ * Outer radius of the arc.
+ */
+ public float outerRadius() { return bb.getFloat(bb_pos + 12); }
+ /**
+ * Number of samples used for drawing the arc.
+ */
+ public int numSamples() { return bb.getInt(bb_pos + 16); }
+
+ public static int createArcDef(FlatBufferBuilder builder, float startAngle, float angleSize, float innerRadius, float outerRadius, int numSamples) {
+ builder.prep(4, 20);
+ builder.putInt(numSamples);
+ builder.putFloat(outerRadius);
+ builder.putFloat(innerRadius);
+ builder.putFloat(angleSize);
+ builder.putFloat(startAngle);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/AxisSystem.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/AxisSystem.java
new file mode 100644
index 0000000..e1dfe30
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/AxisSystem.java
@@ -0,0 +1,41 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+/**
+ * List of different ways to interpret the orientation axis of an asset when
+ * importing it.
+ */
+public final class AxisSystem {
+ private AxisSystem() { }
+ public static final int Unspecified = -1;
+ public static final int XUp_YFront_ZLeft = 0;
+ public static final int XUp_YFront_ZRight = 1;
+ public static final int XUp_YBack_ZLeft = 2;
+ public static final int XUp_YBack_ZRight = 3;
+ public static final int XUp_ZFront_YLeft = 4;
+ public static final int XUp_ZFront_YRight = 5;
+ public static final int XUp_ZBack_YLeft = 6;
+ public static final int XUp_ZBack_YRight = 7;
+ public static final int YUp_XFront_ZLeft = 8;
+ public static final int YUp_XFront_ZRight = 9;
+ public static final int YUp_XBack_ZLeft = 10;
+ public static final int YUp_XBack_ZRight = 11;
+ public static final int YUp_ZFront_XLeft = 12;
+ public static final int YUp_ZFront_XRight = 13;
+ public static final int YUp_ZBack_XLeft = 14;
+ public static final int YUp_ZBack_XRight = 15;
+ public static final int ZUp_XFront_YLeft = 16;
+ public static final int ZUp_XFront_YRight = 17;
+ public static final int ZUp_XBack_YLeft = 18;
+ public static final int ZUp_XBack_YRight = 19;
+ public static final int ZUp_YFront_XLeft = 20;
+ public static final int ZUp_YFront_XRight = 21;
+ public static final int ZUp_YBack_XLeft = 22;
+ public static final int ZUp_YBack_XRight = 23;
+
+ public static final String[] names = { "Unspecified", "XUp_YFront_ZLeft", "XUp_YFront_ZRight", "XUp_YBack_ZLeft", "XUp_YBack_ZRight", "XUp_ZFront_YLeft", "XUp_ZFront_YRight", "XUp_ZBack_YLeft", "XUp_ZBack_YRight", "YUp_XFront_ZLeft", "YUp_XFront_ZRight", "YUp_XBack_ZLeft", "YUp_XBack_ZRight", "YUp_ZFront_XLeft", "YUp_ZFront_XRight", "YUp_ZBack_XLeft", "YUp_ZBack_XRight", "ZUp_XFront_YLeft", "ZUp_XFront_YRight", "ZUp_XBack_YLeft", "ZUp_XBack_YRight", "ZUp_YFront_XLeft", "ZUp_YFront_XRight", "ZUp_YBack_XLeft", "ZUp_YBack_XRight", };
+
+ public static String name(int e) { return names[e - Unspecified]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/BlendShape.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/BlendShape.java
new file mode 100644
index 0000000..92842b5
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/BlendShape.java
@@ -0,0 +1,119 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Matches the format of the original mesh, but potentially supplies new
+ * normals and positions for each vertex. This is used by BlendSystem to
+ * support blend shape animation (morph target animation) in Lullaby.
+ * If a blend shape vertex is identical to the base mesh vertex OR only differs
+ * from the original mesh vertex in its Tangent attribute, the BlendShape only
+ * stores the original mesh index and the Tangent data to reduce the overall
+ * asset size. The vertex will be re-generated at load time.
+ */
+public final class BlendShape extends Table {
+ public static BlendShape getRootAsBlendShape(ByteBuffer _bb) { return getRootAsBlendShape(_bb, new BlendShape()); }
+ public static BlendShape getRootAsBlendShape(ByteBuffer _bb, BlendShape obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public BlendShape __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The name of this blend shape.
+ */
+ public long name() { int o = __offset(4); return o != 0 ? (long)bb.getInt(o + bb_pos) & 0xFFFFFFFFL : 0L; }
+ /**
+ * Contents vertex data of mesh vertices but with positions and normals
+ * adjusted to match this blend shape. Store only those vertices that differ
+ * from mesh vertices in attributes beyond Tangent.
+ */
+ public int vertexData(int j) { int o = __offset(6); return o != 0 ? bb.get(__vector(o) + j * 1) & 0xFF : 0; }
+ public int vertexDataLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer vertexDataAsByteBuffer() { return __vector_as_bytebuffer(6, 1); }
+ public ByteBuffer vertexDataInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 1); }
+ /**
+ * Indices of fully stored vertices. Will either be an array of 16-bit or
+ * 32-bit values.
+ */
+ public long vertexIndices32(int j) { int o = __offset(8); return o != 0 ? (long)bb.getInt(__vector(o) + j * 4) & 0xFFFFFFFFL : 0; }
+ public int vertexIndices32Length() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer vertexIndices32AsByteBuffer() { return __vector_as_bytebuffer(8, 4); }
+ public ByteBuffer vertexIndices32InByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 4); }
+ public int vertexIndices16(int j) { int o = __offset(10); return o != 0 ? bb.getShort(__vector(o) + j * 2) & 0xFFFF : 0; }
+ public int vertexIndices16Length() { int o = __offset(10); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer vertexIndices16AsByteBuffer() { return __vector_as_bytebuffer(10, 2); }
+ public ByteBuffer vertexIndices16InByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 10, 2); }
+ /**
+ * Contains Tangent data that is necessary to restore original values of
+ * blend shape vertices that differ from correspondent mesh vertices in
+ * Tangent attribute only OR indentical ones.
+ */
+ public int tangentData(int j) { int o = __offset(12); return o != 0 ? bb.get(__vector(o) + j * 1) & 0xFF : 0; }
+ public int tangentDataLength() { int o = __offset(12); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer tangentDataAsByteBuffer() { return __vector_as_bytebuffer(12, 1); }
+ public ByteBuffer tangentDataInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 12, 1); }
+ /**
+ * Indices of vertices that differ in Tangent OR indentical ones. Will either
+ * be an array of 16-bit or 32-bit values.
+ */
+ public long tangentIndices32(int j) { int o = __offset(14); return o != 0 ? (long)bb.getInt(__vector(o) + j * 4) & 0xFFFFFFFFL : 0; }
+ public int tangentIndices32Length() { int o = __offset(14); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer tangentIndices32AsByteBuffer() { return __vector_as_bytebuffer(14, 4); }
+ public ByteBuffer tangentIndices32InByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 14, 4); }
+ public int tangentIndices16(int j) { int o = __offset(16); return o != 0 ? bb.getShort(__vector(o) + j * 2) & 0xFFFF : 0; }
+ public int tangentIndices16Length() { int o = __offset(16); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer tangentIndices16AsByteBuffer() { return __vector_as_bytebuffer(16, 2); }
+ public ByteBuffer tangentIndices16InByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 16, 2); }
+
+ public static int createBlendShape(FlatBufferBuilder builder,
+ long name,
+ int vertex_dataOffset,
+ int vertex_indices32Offset,
+ int vertex_indices16Offset,
+ int tangent_dataOffset,
+ int tangent_indices32Offset,
+ int tangent_indices16Offset) {
+ builder.startObject(7);
+ BlendShape.addTangentIndices16(builder, tangent_indices16Offset);
+ BlendShape.addTangentIndices32(builder, tangent_indices32Offset);
+ BlendShape.addTangentData(builder, tangent_dataOffset);
+ BlendShape.addVertexIndices16(builder, vertex_indices16Offset);
+ BlendShape.addVertexIndices32(builder, vertex_indices32Offset);
+ BlendShape.addVertexData(builder, vertex_dataOffset);
+ BlendShape.addName(builder, name);
+ return BlendShape.endBlendShape(builder);
+ }
+
+ public static void startBlendShape(FlatBufferBuilder builder) { builder.startObject(7); }
+ public static void addName(FlatBufferBuilder builder, long name) { builder.addInt(0, (int)name, (int)0L); }
+ public static void addVertexData(FlatBufferBuilder builder, int vertexDataOffset) { builder.addOffset(1, vertexDataOffset, 0); }
+ public static int createVertexDataVector(FlatBufferBuilder builder, byte[] data) { return builder.createByteVector(data); }
+ public static int createVertexDataVector(FlatBufferBuilder builder, ByteBuffer data) { return builder.createByteVector(data); }
+ public static void startVertexDataVector(FlatBufferBuilder builder, int numElems) { builder.startVector(1, numElems, 1); }
+ public static void addVertexIndices32(FlatBufferBuilder builder, int vertexIndices32Offset) { builder.addOffset(2, vertexIndices32Offset, 0); }
+ public static int createVertexIndices32Vector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); }
+ public static void startVertexIndices32Vector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addVertexIndices16(FlatBufferBuilder builder, int vertexIndices16Offset) { builder.addOffset(3, vertexIndices16Offset, 0); }
+ public static int createVertexIndices16Vector(FlatBufferBuilder builder, short[] data) { builder.startVector(2, data.length, 2); for (int i = data.length - 1; i >= 0; i--) builder.addShort(data[i]); return builder.endVector(); }
+ public static void startVertexIndices16Vector(FlatBufferBuilder builder, int numElems) { builder.startVector(2, numElems, 2); }
+ public static void addTangentData(FlatBufferBuilder builder, int tangentDataOffset) { builder.addOffset(4, tangentDataOffset, 0); }
+ public static int createTangentDataVector(FlatBufferBuilder builder, byte[] data) { return builder.createByteVector(data); }
+ public static int createTangentDataVector(FlatBufferBuilder builder, ByteBuffer data) { return builder.createByteVector(data); }
+ public static void startTangentDataVector(FlatBufferBuilder builder, int numElems) { builder.startVector(1, numElems, 1); }
+ public static void addTangentIndices32(FlatBufferBuilder builder, int tangentIndices32Offset) { builder.addOffset(5, tangentIndices32Offset, 0); }
+ public static int createTangentIndices32Vector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); }
+ public static void startTangentIndices32Vector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addTangentIndices16(FlatBufferBuilder builder, int tangentIndices16Offset) { builder.addOffset(6, tangentIndices16Offset, 0); }
+ public static int createTangentIndices16Vector(FlatBufferBuilder builder, short[] data) { builder.startVector(2, data.length, 2); for (int i = data.length - 1; i >= 0; i--) builder.addShort(data[i]); return builder.endVector(); }
+ public static void startTangentIndices16Vector(FlatBufferBuilder builder, int numElems) { builder.startVector(2, numElems, 2); }
+ public static int endBlendShape(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Color.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Color.java
new file mode 100644
index 0000000..5a81b5a
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Color.java
@@ -0,0 +1,29 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Color extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public Color __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public float r() { return bb.getFloat(bb_pos + 0); }
+ public float g() { return bb.getFloat(bb_pos + 4); }
+ public float b() { return bb.getFloat(bb_pos + 8); }
+ public float a() { return bb.getFloat(bb_pos + 12); }
+
+ public static int createColor(FlatBufferBuilder builder, float r, float g, float b, float a) {
+ builder.prep(4, 16);
+ builder.putFloat(a);
+ builder.putFloat(b);
+ builder.putFloat(g);
+ builder.putFloat(r);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataBool.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataBool.java
new file mode 100644
index 0000000..1b1023d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataBool.java
@@ -0,0 +1,36 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for boolean values to be stored in a VariantDef.
+ */
+public final class DataBool extends Table {
+ public static DataBool getRootAsDataBool(ByteBuffer _bb) { return getRootAsDataBool(_bb, new DataBool()); }
+ public static DataBool getRootAsDataBool(ByteBuffer _bb, DataBool obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataBool __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public boolean value() { int o = __offset(4); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+
+ public static int createDataBool(FlatBufferBuilder builder,
+ boolean value) {
+ builder.startObject(1);
+ DataBool.addValue(builder, value);
+ return DataBool.endDataBool(builder);
+ }
+
+ public static void startDataBool(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, boolean value) { builder.addBoolean(0, value, false); }
+ public static int endDataBool(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataBytes.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataBytes.java
new file mode 100644
index 0000000..553fc32
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataBytes.java
@@ -0,0 +1,42 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for arbitrary binary data to be stored in a VariantDef.
+ */
+public final class DataBytes extends Table {
+ public static DataBytes getRootAsDataBytes(ByteBuffer _bb) { return getRootAsDataBytes(_bb, new DataBytes()); }
+ public static DataBytes getRootAsDataBytes(ByteBuffer _bb, DataBytes obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataBytes __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public int value(int j) { int o = __offset(4); return o != 0 ? bb.get(__vector(o) + j * 1) & 0xFF : 0; }
+ public int valueLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer valueAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer valueInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+
+ public static int createDataBytes(FlatBufferBuilder builder,
+ int valueOffset) {
+ builder.startObject(1);
+ DataBytes.addValue(builder, valueOffset);
+ return DataBytes.endDataBytes(builder);
+ }
+
+ public static void startDataBytes(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, int valueOffset) { builder.addOffset(0, valueOffset, 0); }
+ public static int createValueVector(FlatBufferBuilder builder, byte[] data) { return builder.createByteVector(data); }
+ public static int createValueVector(FlatBufferBuilder builder, ByteBuffer data) { return builder.createByteVector(data); }
+ public static void startValueVector(FlatBufferBuilder builder, int numElems) { builder.startVector(1, numElems, 1); }
+ public static int endDataBytes(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataFloat.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataFloat.java
new file mode 100644
index 0000000..fcce462
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataFloat.java
@@ -0,0 +1,36 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for floating-point values to be stored in a VariantDef.
+ */
+public final class DataFloat extends Table {
+ public static DataFloat getRootAsDataFloat(ByteBuffer _bb) { return getRootAsDataFloat(_bb, new DataFloat()); }
+ public static DataFloat getRootAsDataFloat(ByteBuffer _bb, DataFloat obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataFloat __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public float value() { int o = __offset(4); return o != 0 ? bb.getFloat(o + bb_pos) : 0.0f; }
+
+ public static int createDataFloat(FlatBufferBuilder builder,
+ float value) {
+ builder.startObject(1);
+ DataFloat.addValue(builder, value);
+ return DataFloat.endDataFloat(builder);
+ }
+
+ public static void startDataFloat(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, float value) { builder.addFloat(0, value, 0.0f); }
+ public static int endDataFloat(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataHashValue.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataHashValue.java
new file mode 100644
index 0000000..6c410bf
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataHashValue.java
@@ -0,0 +1,36 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for hash values to be stored in a VariantDef.
+ */
+public final class DataHashValue extends Table {
+ public static DataHashValue getRootAsDataHashValue(ByteBuffer _bb) { return getRootAsDataHashValue(_bb, new DataHashValue()); }
+ public static DataHashValue getRootAsDataHashValue(ByteBuffer _bb, DataHashValue obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataHashValue __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public long value() { int o = __offset(4); return o != 0 ? (long)bb.getInt(o + bb_pos) & 0xFFFFFFFFL : 0L; }
+
+ public static int createDataHashValue(FlatBufferBuilder builder,
+ long value) {
+ builder.startObject(1);
+ DataHashValue.addValue(builder, value);
+ return DataHashValue.endDataHashValue(builder);
+ }
+
+ public static void startDataHashValue(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, long value) { builder.addInt(0, (int)value, (int)0L); }
+ public static int endDataHashValue(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataInt.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataInt.java
new file mode 100644
index 0000000..2b2d0bb
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataInt.java
@@ -0,0 +1,36 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for integer values to be stored in a VariantDef.
+ */
+public final class DataInt extends Table {
+ public static DataInt getRootAsDataInt(ByteBuffer _bb) { return getRootAsDataInt(_bb, new DataInt()); }
+ public static DataInt getRootAsDataInt(ByteBuffer _bb, DataInt obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataInt __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public int value() { int o = __offset(4); return o != 0 ? bb.getInt(o + bb_pos) : 0; }
+
+ public static int createDataInt(FlatBufferBuilder builder,
+ int value) {
+ builder.startObject(1);
+ DataInt.addValue(builder, value);
+ return DataInt.endDataInt(builder);
+ }
+
+ public static void startDataInt(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, int value) { builder.addInt(0, value, 0); }
+ public static int endDataInt(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataQuat.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataQuat.java
new file mode 100644
index 0000000..da1cdd2
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataQuat.java
@@ -0,0 +1,30 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for quaternion values to be stored in a VariantDef.
+ */
+public final class DataQuat extends Table {
+ public static DataQuat getRootAsDataQuat(ByteBuffer _bb) { return getRootAsDataQuat(_bb, new DataQuat()); }
+ public static DataQuat getRootAsDataQuat(ByteBuffer _bb, DataQuat obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataQuat __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public Quat value() { return value(new Quat()); }
+ public Quat value(Quat obj) { int o = __offset(4); return o != 0 ? obj.__assign(o + bb_pos, bb) : null; }
+
+ public static void startDataQuat(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, int valueOffset) { builder.addStruct(0, valueOffset, 0); }
+ public static int endDataQuat(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataString.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataString.java
new file mode 100644
index 0000000..ab80a76
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataString.java
@@ -0,0 +1,38 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for string values to be stored in a VariantDef.
+ */
+public final class DataString extends Table {
+ public static DataString getRootAsDataString(ByteBuffer _bb) { return getRootAsDataString(_bb, new DataString()); }
+ public static DataString getRootAsDataString(ByteBuffer _bb, DataString obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataString __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public String value() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer valueAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer valueInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+
+ public static int createDataString(FlatBufferBuilder builder,
+ int valueOffset) {
+ builder.startObject(1);
+ DataString.addValue(builder, valueOffset);
+ return DataString.endDataString(builder);
+ }
+
+ public static void startDataString(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, int valueOffset) { builder.addOffset(0, valueOffset, 0); }
+ public static int endDataString(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataVec2.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataVec2.java
new file mode 100644
index 0000000..c5ef311
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataVec2.java
@@ -0,0 +1,30 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for 2-dimensional vector values to be stored in a VariantDef.
+ */
+public final class DataVec2 extends Table {
+ public static DataVec2 getRootAsDataVec2(ByteBuffer _bb) { return getRootAsDataVec2(_bb, new DataVec2()); }
+ public static DataVec2 getRootAsDataVec2(ByteBuffer _bb, DataVec2 obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataVec2 __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public Vec2 value() { return value(new Vec2()); }
+ public Vec2 value(Vec2 obj) { int o = __offset(4); return o != 0 ? obj.__assign(o + bb_pos, bb) : null; }
+
+ public static void startDataVec2(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, int valueOffset) { builder.addStruct(0, valueOffset, 0); }
+ public static int endDataVec2(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataVec3.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataVec3.java
new file mode 100644
index 0000000..c243d08
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataVec3.java
@@ -0,0 +1,30 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for 3-dimensional vector values to be stored in a VariantDef.
+ */
+public final class DataVec3 extends Table {
+ public static DataVec3 getRootAsDataVec3(ByteBuffer _bb) { return getRootAsDataVec3(_bb, new DataVec3()); }
+ public static DataVec3 getRootAsDataVec3(ByteBuffer _bb, DataVec3 obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataVec3 __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public Vec3 value() { return value(new Vec3()); }
+ public Vec3 value(Vec3 obj) { int o = __offset(4); return o != 0 ? obj.__assign(o + bb_pos, bb) : null; }
+
+ public static void startDataVec3(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, int valueOffset) { builder.addStruct(0, valueOffset, 0); }
+ public static int endDataVec3(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataVec4.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataVec4.java
new file mode 100644
index 0000000..9bedc8c
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DataVec4.java
@@ -0,0 +1,30 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Data type for 4-dimensional vector values to be stored in a VariantDef.
+ */
+public final class DataVec4 extends Table {
+ public static DataVec4 getRootAsDataVec4(ByteBuffer _bb) { return getRootAsDataVec4(_bb, new DataVec4()); }
+ public static DataVec4 getRootAsDataVec4(ByteBuffer _bb, DataVec4 obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public DataVec4 __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public Vec4 value() { return value(new Vec4()); }
+ public Vec4 value(Vec4 obj) { int o = __offset(4); return o != 0 ? obj.__assign(o + bb_pos, bb) : null; }
+
+ public static void startDataVec4(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValue(FlatBufferBuilder builder, int valueOffset) { builder.addStruct(0, valueOffset, 0); }
+ public static int endDataVec4(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DeviceType.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DeviceType.java
new file mode 100644
index 0000000..1e4fb27
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/DeviceType.java
@@ -0,0 +1,21 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+/**
+ * Potential Input Devices (matches enum in input_manager.h)
+ */
+public final class DeviceType {
+ private DeviceType() { }
+ public static final int Hmd = 0;
+ public static final int Mouse = 1;
+ public static final int Keyboard = 2;
+ public static final int Controller = 3;
+ public static final int Controller2 = 4;
+ public static final int Hand = 5;
+
+ public static final String[] names = { "Hmd", "Mouse", "Keyboard", "Controller", "Controller2", "Hand", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/KeyVariantPairDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/KeyVariantPairDef.java
new file mode 100644
index 0000000..faf25e7
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/KeyVariantPairDef.java
@@ -0,0 +1,51 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Internal table for VariantMapDef that associates a hash-value key with a
+ * variant type.
+ */
+public final class KeyVariantPairDef extends Table {
+ public static KeyVariantPairDef getRootAsKeyVariantPairDef(ByteBuffer _bb) { return getRootAsKeyVariantPairDef(_bb, new KeyVariantPairDef()); }
+ public static KeyVariantPairDef getRootAsKeyVariantPairDef(ByteBuffer _bb, KeyVariantPairDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public KeyVariantPairDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public String key() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer keyAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer keyInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+ public long hashKey() { int o = __offset(6); return o != 0 ? (long)bb.getInt(o + bb_pos) & 0xFFFFFFFFL : 0L; }
+ public byte valueType() { int o = __offset(8); return o != 0 ? bb.get(o + bb_pos) : 0; }
+ public Table value(Table obj) { int o = __offset(10); return o != 0 ? __union(obj, o) : null; }
+
+ public static int createKeyVariantPairDef(FlatBufferBuilder builder,
+ int keyOffset,
+ long hash_key,
+ byte value_type,
+ int valueOffset) {
+ builder.startObject(4);
+ KeyVariantPairDef.addValue(builder, valueOffset);
+ KeyVariantPairDef.addHashKey(builder, hash_key);
+ KeyVariantPairDef.addKey(builder, keyOffset);
+ KeyVariantPairDef.addValueType(builder, value_type);
+ return KeyVariantPairDef.endKeyVariantPairDef(builder);
+ }
+
+ public static void startKeyVariantPairDef(FlatBufferBuilder builder) { builder.startObject(4); }
+ public static void addKey(FlatBufferBuilder builder, int keyOffset) { builder.addOffset(0, keyOffset, 0); }
+ public static void addHashKey(FlatBufferBuilder builder, long hashKey) { builder.addInt(1, (int)hashKey, (int)0L); }
+ public static void addValueType(FlatBufferBuilder builder, byte valueType) { builder.addByte(2, valueType, 0); }
+ public static void addValue(FlatBufferBuilder builder, int valueOffset) { builder.addOffset(3, valueOffset, 0); }
+ public static int endKeyVariantPairDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/LayoutFillOrder.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/LayoutFillOrder.java
new file mode 100644
index 0000000..8fc8674
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/LayoutFillOrder.java
@@ -0,0 +1,58 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+/**
+ * The directions a layout will grow in.
+ * This controls the ordering of elements within wrapping rows, but not the
+ * horizontal alignment of those rows as a whole (which is controlled by
+ * LayoutHorizontalAlignment).
+ */
+public final class LayoutFillOrder {
+ private LayoutFillOrder() { }
+ /**
+ * The first entity is added leftmost, and subsequent entities go to the
+ * right (and then down if wrapping is enabled).
+ */
+ public static final int RightDown = 0;
+ /**
+ * The first entity is added rightmost, and subsequent entities go to the
+ * left (and then down if wrapping is enabled).
+ */
+ public static final int LeftDown = 1;
+ /**
+ * The first entity is added leftmost, and subsequent entities go down
+ * (and then right if wrapping is enabled).
+ */
+ public static final int DownRight = 2;
+ /**
+ * The first entity is added rightmost, and subsequent entities go down
+ * (and then left if wrapping is enabled).
+ */
+ public static final int DownLeft = 3;
+ /**
+ * The first entity is added leftmost, and subsequent entities go to the
+ * right (and then up if wrapping is enabled).
+ */
+ public static final int RightUp = 4;
+ /**
+ * The first entity is added rightmost, and subsequent entities go to the
+ * left (and then up if wrapping is enabled).
+ */
+ public static final int LeftUp = 5;
+ /**
+ * The first entity is added leftmost, and subsequent entities go up
+ * (and then right if wrapping is enabled).
+ */
+ public static final int UpRight = 6;
+ /**
+ * The first entity is added rightmost, and subsequent entities go up
+ * (and then left if wrapping is enabled).
+ */
+ public static final int UpLeft = 7;
+
+ public static final String[] names = { "RightDown", "LeftDown", "DownRight", "DownLeft", "RightUp", "LeftUp", "UpRight", "UpLeft", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/LayoutHorizontalAlignment.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/LayoutHorizontalAlignment.java
new file mode 100644
index 0000000..f5b7ebb
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/LayoutHorizontalAlignment.java
@@ -0,0 +1,29 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+/**
+ * The anchor at which the entities will be aligned relative to the canvas.
+ */
+public final class LayoutHorizontalAlignment {
+ private LayoutHorizontalAlignment() { }
+ /**
+ * The left border of the leftmost element will align to the left side of the
+ * canvas.
+ */
+ public static final int Left = 0;
+ /**
+ * The layout will be centered horizontally on the canvas.
+ */
+ public static final int Center = 1;
+ /**
+ * The right border of the rightmost element will align to the right side of
+ * the canvas.
+ */
+ public static final int Right = 2;
+
+ public static final String[] names = { "Left", "Center", "Right", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/LayoutVerticalAlignment.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/LayoutVerticalAlignment.java
new file mode 100644
index 0000000..6784bc9
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/LayoutVerticalAlignment.java
@@ -0,0 +1,30 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+/**
+ * The anchor at which the entities will be aligned *within* each row.
+ * Note that rows collectively are still top aligned relative to the canvas.
+ * To center align within the canvas, add a placeholder child entity with the
+ * same height as the canvas.
+ */
+public final class LayoutVerticalAlignment {
+ private LayoutVerticalAlignment() { }
+ /**
+ * Each entity will align to the top of its row in the layout.
+ */
+ public static final int Top = 0;
+ /**
+ * Each entity will be centered within its row.
+ */
+ public static final int Center = 1;
+ /**
+ * Each entity will align to the bottom of its row.
+ */
+ public static final int Bottom = 2;
+
+ public static final String[] names = { "Top", "Center", "Bottom", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Mat4x3.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Mat4x3.java
new file mode 100644
index 0000000..b5a3893
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Mat4x3.java
@@ -0,0 +1,42 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Mat4x3 extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public Mat4x3 __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public Vec4 c0() { return c0(new Vec4()); }
+ public Vec4 c0(Vec4 obj) { return obj.__assign(bb_pos + 0, bb); }
+ public Vec4 c1() { return c1(new Vec4()); }
+ public Vec4 c1(Vec4 obj) { return obj.__assign(bb_pos + 16, bb); }
+ public Vec4 c2() { return c2(new Vec4()); }
+ public Vec4 c2(Vec4 obj) { return obj.__assign(bb_pos + 32, bb); }
+
+ public static int createMat4x3(FlatBufferBuilder builder, float c0_x, float c0_y, float c0_z, float c0_w, float c1_x, float c1_y, float c1_z, float c1_w, float c2_x, float c2_y, float c2_z, float c2_w) {
+ builder.prep(4, 48);
+ builder.prep(4, 16);
+ builder.putFloat(c2_w);
+ builder.putFloat(c2_z);
+ builder.putFloat(c2_y);
+ builder.putFloat(c2_x);
+ builder.prep(4, 16);
+ builder.putFloat(c1_w);
+ builder.putFloat(c1_z);
+ builder.putFloat(c1_y);
+ builder.putFloat(c1_x);
+ builder.prep(4, 16);
+ builder.putFloat(c0_w);
+ builder.putFloat(c0_z);
+ builder.putFloat(c0_y);
+ builder.putFloat(c0_x);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/MaterialDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/MaterialDef.java
new file mode 100644
index 0000000..96cce27
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/MaterialDef.java
@@ -0,0 +1,64 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * A description of the Material to be used by the RenderSystem when drawing
+ * an Entity. A Material describes the "look" applied to a single surface
+ * of a mesh.
+ */
+public final class MaterialDef extends Table {
+ public static MaterialDef getRootAsMaterialDef(ByteBuffer _bb) { return getRootAsMaterialDef(_bb, new MaterialDef()); }
+ public static MaterialDef getRootAsMaterialDef(ByteBuffer _bb, MaterialDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public MaterialDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The name of the material.
+ */
+ public String name() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer nameAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer nameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+ /**
+ * A dictionary of all material properties extracted from the source file.
+ * These properties are interpretted by the RenderSystem to create the
+ * appropriate Material.
+ */
+ public VariantMapDef properties() { return properties(new VariantMapDef()); }
+ public VariantMapDef properties(VariantMapDef obj) { int o = __offset(6); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; }
+ /**
+ * The list of textures associated with the Material.
+ */
+ public MaterialTextureDef textures(int j) { return textures(new MaterialTextureDef(), j); }
+ public MaterialTextureDef textures(MaterialTextureDef obj, int j) { int o = __offset(8); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int texturesLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; }
+
+ public static int createMaterialDef(FlatBufferBuilder builder,
+ int nameOffset,
+ int propertiesOffset,
+ int texturesOffset) {
+ builder.startObject(3);
+ MaterialDef.addTextures(builder, texturesOffset);
+ MaterialDef.addProperties(builder, propertiesOffset);
+ MaterialDef.addName(builder, nameOffset);
+ return MaterialDef.endMaterialDef(builder);
+ }
+
+ public static void startMaterialDef(FlatBufferBuilder builder) { builder.startObject(3); }
+ public static void addName(FlatBufferBuilder builder, int nameOffset) { builder.addOffset(0, nameOffset, 0); }
+ public static void addProperties(FlatBufferBuilder builder, int propertiesOffset) { builder.addOffset(1, propertiesOffset, 0); }
+ public static void addTextures(FlatBufferBuilder builder, int texturesOffset) { builder.addOffset(2, texturesOffset, 0); }
+ public static int createTexturesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startTexturesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endMaterialDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/MaterialTextureDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/MaterialTextureDef.java
new file mode 100644
index 0000000..c894b5e
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/MaterialTextureDef.java
@@ -0,0 +1,52 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class MaterialTextureDef extends Table {
+ public static MaterialTextureDef getRootAsMaterialTextureDef(ByteBuffer _bb) { return getRootAsMaterialTextureDef(_bb, new MaterialTextureDef()); }
+ public static MaterialTextureDef getRootAsMaterialTextureDef(ByteBuffer _bb, MaterialTextureDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public MaterialTextureDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public String name() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer nameAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer nameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+ public int usage() { int o = __offset(6); return o != 0 ? bb.getInt(o + bb_pos) : 0; }
+ /**
+ * For textures with multiple usages, this array describes the usage of each
+ * channel.
+ */
+ public int usagePerChannel(int j) { int o = __offset(8); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; }
+ public int usagePerChannelLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer usagePerChannelAsByteBuffer() { return __vector_as_bytebuffer(8, 4); }
+ public ByteBuffer usagePerChannelInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 4); }
+
+ public static int createMaterialTextureDef(FlatBufferBuilder builder,
+ int nameOffset,
+ int usage,
+ int usage_per_channelOffset) {
+ builder.startObject(3);
+ MaterialTextureDef.addUsagePerChannel(builder, usage_per_channelOffset);
+ MaterialTextureDef.addUsage(builder, usage);
+ MaterialTextureDef.addName(builder, nameOffset);
+ return MaterialTextureDef.endMaterialTextureDef(builder);
+ }
+
+ public static void startMaterialTextureDef(FlatBufferBuilder builder) { builder.startObject(3); }
+ public static void addName(FlatBufferBuilder builder, int nameOffset) { builder.addOffset(0, nameOffset, 0); }
+ public static void addUsage(FlatBufferBuilder builder, int usage) { builder.addInt(1, usage, 0); }
+ public static void addUsagePerChannel(FlatBufferBuilder builder, int usagePerChannelOffset) { builder.addOffset(2, usagePerChannelOffset, 0); }
+ public static int createUsagePerChannelVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); }
+ public static void startUsagePerChannelVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endMaterialTextureDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/MaterialTextureUsage.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/MaterialTextureUsage.java
new file mode 100644
index 0000000..7bf4e28
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/MaterialTextureUsage.java
@@ -0,0 +1,28 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+public final class MaterialTextureUsage {
+ private MaterialTextureUsage() { }
+ public static final int BaseColor = 0;
+ public static final int Metallic = 1;
+ public static final int Normal = 2;
+ public static final int Bump = 3;
+ public static final int Height = 4;
+ public static final int Specular = 5;
+ public static final int Ambient = 6;
+ public static final int Emissive = 7;
+ public static final int Light = 8;
+ public static final int Shadow = 9;
+ public static final int Reflection = 10;
+ public static final int Opacity = 11;
+ public static final int Roughness = 12;
+ public static final int Occlusion = 13;
+ public static final int Shininess = 14;
+ public static final int BrdfLookupTable = 15;
+ public static final int DiffuseEnvironment = 16;
+ public static final int SpecularEnvironment = 17;
+ public static final int DiffuseColor = 18;
+ public static final int Unused = 255;
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelDef.java
new file mode 100644
index 0000000..8280ff8
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelDef.java
@@ -0,0 +1,68 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Contains all the information stored in a lullmodel file. This information
+ * will be used to initialize various Systems from the same asset file. For
+ * example, the skeleton data will be used by the RigSystem, while the vertex
+ * data will be used by the RenderSystem.
+ */
+public final class ModelDef extends Table {
+ public static ModelDef getRootAsModelDef(ByteBuffer _bb) { return getRootAsModelDef(_bb, new ModelDef()); }
+ public static ModelDef getRootAsModelDef(ByteBuffer _bb, ModelDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public ModelDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * Version number to help decide how to interpret the flatbuffer data.
+ */
+ public int version() { int o = __offset(4); return o != 0 ? bb.getInt(o + bb_pos) : 1; }
+ /**
+ * Model data for different LODs.
+ */
+ public ModelInstanceDef lods(int j) { return lods(new ModelInstanceDef(), j); }
+ public ModelInstanceDef lods(ModelInstanceDef obj, int j) { int o = __offset(6); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int lodsLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * The skeletal information used by the RigSystem to support skinned
+ * animations.
+ */
+ public SkeletonDef skeleton() { return skeleton(new SkeletonDef()); }
+ public SkeletonDef skeleton(SkeletonDef obj) { int o = __offset(8); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; }
+ /**
+ * The minimum and maximum bounds contained in the vertex data.
+ */
+ public AabbDef boundingBox() { return boundingBox(new AabbDef()); }
+ public AabbDef boundingBox(AabbDef obj) { int o = __offset(10); return o != 0 ? obj.__assign(o + bb_pos, bb) : null; }
+ /**
+ * A collection of embedded textures associated with this model.
+ */
+ public TextureDef textures(int j) { return textures(new TextureDef(), j); }
+ public TextureDef textures(TextureDef obj, int j) { int o = __offset(12); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int texturesLength() { int o = __offset(12); return o != 0 ? __vector_len(o) : 0; }
+
+ public static void startModelDef(FlatBufferBuilder builder) { builder.startObject(5); }
+ public static void addVersion(FlatBufferBuilder builder, int version) { builder.addInt(0, version, 1); }
+ public static void addLods(FlatBufferBuilder builder, int lodsOffset) { builder.addOffset(1, lodsOffset, 0); }
+ public static int createLodsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startLodsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addSkeleton(FlatBufferBuilder builder, int skeletonOffset) { builder.addOffset(2, skeletonOffset, 0); }
+ public static void addBoundingBox(FlatBufferBuilder builder, int boundingBoxOffset) { builder.addStruct(3, boundingBoxOffset, 0); }
+ public static void addTextures(FlatBufferBuilder builder, int texturesOffset) { builder.addOffset(4, texturesOffset, 0); }
+ public static int createTexturesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startTexturesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endModelDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+ public static void finishModelDefBuffer(FlatBufferBuilder builder, int offset) { builder.finish(offset); }
+ public static void finishSizePrefixedModelDefBuffer(FlatBufferBuilder builder, int offset) { builder.finishSizePrefixed(offset); }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelIndexRange.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelIndexRange.java
new file mode 100644
index 0000000..63a5677
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelIndexRange.java
@@ -0,0 +1,28 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * The range of indices associated with a single draw call.
+ */
+public final class ModelIndexRange extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public ModelIndexRange __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public long start() { return (long)bb.getInt(bb_pos + 0) & 0xFFFFFFFFL; }
+ public long end() { return (long)bb.getInt(bb_pos + 4) & 0xFFFFFFFFL; }
+
+ public static int createModelIndexRange(FlatBufferBuilder builder, long start, long end) {
+ builder.prep(4, 8);
+ builder.putInt((int)end);
+ builder.putInt((int)start);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelInstanceDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelInstanceDef.java
new file mode 100644
index 0000000..4bffc8e
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelInstanceDef.java
@@ -0,0 +1,159 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * A single instance of model data used to render an object at a given LOD.
+ */
+public final class ModelInstanceDef extends Table {
+ public static ModelInstanceDef getRootAsModelInstanceDef(ByteBuffer _bb) { return getRootAsModelInstanceDef(_bb, new ModelInstanceDef()); }
+ public static ModelInstanceDef getRootAsModelInstanceDef(ByteBuffer _bb, ModelInstanceDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public ModelInstanceDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The "raw" vertex data stored as a byte array.
+ */
+ public int vertexData(int j) { int o = __offset(4); return o != 0 ? bb.get(__vector(o) + j * 1) & 0xFF : 0; }
+ public int vertexDataLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer vertexDataAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer vertexDataInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+ /**
+ * Indices into the vertex data. Will either be an array of 16-bit or 32-bit
+ * values.
+ */
+ public int indices16(int j) { int o = __offset(6); return o != 0 ? bb.getShort(__vector(o) + j * 2) & 0xFFFF : 0; }
+ public int indices16Length() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer indices16AsByteBuffer() { return __vector_as_bytebuffer(6, 2); }
+ public ByteBuffer indices16InByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 2); }
+ public long indices32(int j) { int o = __offset(8); return o != 0 ? (long)bb.getInt(__vector(o) + j * 4) & 0xFFFFFFFFL : 0; }
+ public int indices32Length() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer indices32AsByteBuffer() { return __vector_as_bytebuffer(8, 4); }
+ public ByteBuffer indices32InByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 4); }
+ /**
+ * The range of indices for each submesh.
+ */
+ public ModelIndexRange ranges(int j) { return ranges(new ModelIndexRange(), j); }
+ public ModelIndexRange ranges(ModelIndexRange obj, int j) { int o = __offset(10); return o != 0 ? obj.__assign(__vector(o) + j * 8, bb) : null; }
+ public int rangesLength() { int o = __offset(10); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * The material describing the "look" of each submesh.
+ */
+ public MaterialDef materials(int j) { return materials(new MaterialDef(), j); }
+ public MaterialDef materials(MaterialDef obj, int j) { int o = __offset(12); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int materialsLength() { int o = __offset(12); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * Describes the structure of the vertex data, effectively the VertexFormat.
+ */
+ public VertexAttribute vertexAttributes(int j) { return vertexAttributes(new VertexAttribute(), j); }
+ public VertexAttribute vertexAttributes(VertexAttribute obj, int j) { int o = __offset(14); return o != 0 ? obj.__assign(__vector(o) + j * 8, bb) : null; }
+ public int vertexAttributesLength() { int o = __offset(14); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * The total number of vertices stored in the vertex data.
+ */
+ public long numVertices() { int o = __offset(16); return o != 0 ? (long)bb.getInt(o + bb_pos) & 0xFFFFFFFFL : 0L; }
+ /**
+ * Whether or not the attributes in the vertex data are interleaved.
+ */
+ public boolean interleaved() { int o = __offset(18); return o != 0 ? 0!=bb.get(o + bb_pos) : true; }
+ /**
+ * Maps the skeleton bone index to the shader bone index. The shader bones
+ * are only the bones that have at least one vertex weighted to them and, as
+ * such, are a subset of all the bones in the skeleton.
+ */
+ public int shaderToMeshBones(int j) { int o = __offset(20); return o != 0 ? bb.get(__vector(o) + j * 1) & 0xFF : 0; }
+ public int shaderToMeshBonesLength() { int o = __offset(20); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer shaderToMeshBonesAsByteBuffer() { return __vector_as_bytebuffer(20, 1); }
+ public ByteBuffer shaderToMeshBonesInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 20, 1); }
+ /**
+ * A collection of blendshapes, if they exist.
+ */
+ public BlendShape blendShapes(int j) { return blendShapes(new BlendShape(), j); }
+ public BlendShape blendShapes(BlendShape obj, int j) { int o = __offset(22); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int blendShapesLength() { int o = __offset(22); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * Describes the structure of the vertex data for the blend shapes.
+ */
+ public VertexAttribute blendAttributes(int j) { return blendAttributes(new VertexAttribute(), j); }
+ public VertexAttribute blendAttributes(VertexAttribute obj, int j) { int o = __offset(24); return o != 0 ? obj.__assign(__vector(o) + j * 8, bb) : null; }
+ public int blendAttributesLength() { int o = __offset(24); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * A bounding Aabb for each submesh.
+ */
+ public SubmeshAabb aabbs(int j) { return aabbs(new SubmeshAabb(), j); }
+ public SubmeshAabb aabbs(SubmeshAabb obj, int j) { int o = __offset(26); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int aabbsLength() { int o = __offset(26); return o != 0 ? __vector_len(o) : 0; }
+
+ public static int createModelInstanceDef(FlatBufferBuilder builder,
+ int vertex_dataOffset,
+ int indices16Offset,
+ int indices32Offset,
+ int rangesOffset,
+ int materialsOffset,
+ int vertex_attributesOffset,
+ long num_vertices,
+ boolean interleaved,
+ int shader_to_mesh_bonesOffset,
+ int blend_shapesOffset,
+ int blend_attributesOffset,
+ int aabbsOffset) {
+ builder.startObject(12);
+ ModelInstanceDef.addAabbs(builder, aabbsOffset);
+ ModelInstanceDef.addBlendAttributes(builder, blend_attributesOffset);
+ ModelInstanceDef.addBlendShapes(builder, blend_shapesOffset);
+ ModelInstanceDef.addShaderToMeshBones(builder, shader_to_mesh_bonesOffset);
+ ModelInstanceDef.addNumVertices(builder, num_vertices);
+ ModelInstanceDef.addVertexAttributes(builder, vertex_attributesOffset);
+ ModelInstanceDef.addMaterials(builder, materialsOffset);
+ ModelInstanceDef.addRanges(builder, rangesOffset);
+ ModelInstanceDef.addIndices32(builder, indices32Offset);
+ ModelInstanceDef.addIndices16(builder, indices16Offset);
+ ModelInstanceDef.addVertexData(builder, vertex_dataOffset);
+ ModelInstanceDef.addInterleaved(builder, interleaved);
+ return ModelInstanceDef.endModelInstanceDef(builder);
+ }
+
+ public static void startModelInstanceDef(FlatBufferBuilder builder) { builder.startObject(12); }
+ public static void addVertexData(FlatBufferBuilder builder, int vertexDataOffset) { builder.addOffset(0, vertexDataOffset, 0); }
+ public static int createVertexDataVector(FlatBufferBuilder builder, byte[] data) { return builder.createByteVector(data); }
+ public static int createVertexDataVector(FlatBufferBuilder builder, ByteBuffer data) { return builder.createByteVector(data); }
+ public static void startVertexDataVector(FlatBufferBuilder builder, int numElems) { builder.startVector(1, numElems, 1); }
+ public static void addIndices16(FlatBufferBuilder builder, int indices16Offset) { builder.addOffset(1, indices16Offset, 0); }
+ public static int createIndices16Vector(FlatBufferBuilder builder, short[] data) { builder.startVector(2, data.length, 2); for (int i = data.length - 1; i >= 0; i--) builder.addShort(data[i]); return builder.endVector(); }
+ public static void startIndices16Vector(FlatBufferBuilder builder, int numElems) { builder.startVector(2, numElems, 2); }
+ public static void addIndices32(FlatBufferBuilder builder, int indices32Offset) { builder.addOffset(2, indices32Offset, 0); }
+ public static int createIndices32Vector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); }
+ public static void startIndices32Vector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addRanges(FlatBufferBuilder builder, int rangesOffset) { builder.addOffset(3, rangesOffset, 0); }
+ public static void startRangesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(8, numElems, 4); }
+ public static void addMaterials(FlatBufferBuilder builder, int materialsOffset) { builder.addOffset(4, materialsOffset, 0); }
+ public static int createMaterialsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startMaterialsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addVertexAttributes(FlatBufferBuilder builder, int vertexAttributesOffset) { builder.addOffset(5, vertexAttributesOffset, 0); }
+ public static void startVertexAttributesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(8, numElems, 4); }
+ public static void addNumVertices(FlatBufferBuilder builder, long numVertices) { builder.addInt(6, (int)numVertices, (int)0L); }
+ public static void addInterleaved(FlatBufferBuilder builder, boolean interleaved) { builder.addBoolean(7, interleaved, true); }
+ public static void addShaderToMeshBones(FlatBufferBuilder builder, int shaderToMeshBonesOffset) { builder.addOffset(8, shaderToMeshBonesOffset, 0); }
+ public static int createShaderToMeshBonesVector(FlatBufferBuilder builder, byte[] data) { return builder.createByteVector(data); }
+ public static int createShaderToMeshBonesVector(FlatBufferBuilder builder, ByteBuffer data) { return builder.createByteVector(data); }
+ public static void startShaderToMeshBonesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(1, numElems, 1); }
+ public static void addBlendShapes(FlatBufferBuilder builder, int blendShapesOffset) { builder.addOffset(9, blendShapesOffset, 0); }
+ public static int createBlendShapesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startBlendShapesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addBlendAttributes(FlatBufferBuilder builder, int blendAttributesOffset) { builder.addOffset(10, blendAttributesOffset, 0); }
+ public static void startBlendAttributesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(8, numElems, 4); }
+ public static void addAabbs(FlatBufferBuilder builder, int aabbsOffset) { builder.addOffset(11, aabbsOffset, 0); }
+ public static int createAabbsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startAabbsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endModelInstanceDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineCollidableDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineCollidableDef.java
new file mode 100644
index 0000000..7def360
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineCollidableDef.java
@@ -0,0 +1,41 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Information about a collidable model.
+ */
+public final class ModelPipelineCollidableDef extends Table {
+ public static ModelPipelineCollidableDef getRootAsModelPipelineCollidableDef(ByteBuffer _bb) { return getRootAsModelPipelineCollidableDef(_bb, new ModelPipelineCollidableDef()); }
+ public static ModelPipelineCollidableDef getRootAsModelPipelineCollidableDef(ByteBuffer _bb, ModelPipelineCollidableDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public ModelPipelineCollidableDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The name of ModelPipelineImportDef defining this model.
+ */
+ public String source() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer sourceAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer sourceInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+
+ public static int createModelPipelineCollidableDef(FlatBufferBuilder builder,
+ int sourceOffset) {
+ builder.startObject(1);
+ ModelPipelineCollidableDef.addSource(builder, sourceOffset);
+ return ModelPipelineCollidableDef.endModelPipelineCollidableDef(builder);
+ }
+
+ public static void startModelPipelineCollidableDef(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addSource(FlatBufferBuilder builder, int sourceOffset) { builder.addOffset(0, sourceOffset, 0); }
+ public static int endModelPipelineCollidableDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineDef.java
new file mode 100644
index 0000000..2a45b2a
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineDef.java
@@ -0,0 +1,83 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * An optional configuration file that can be provided to the model pipeline
+ * to control behaviour.
+ */
+public final class ModelPipelineDef extends Table {
+ public static ModelPipelineDef getRootAsModelPipelineDef(ByteBuffer _bb) { return getRootAsModelPipelineDef(_bb, new ModelPipelineDef()); }
+ public static ModelPipelineDef getRootAsModelPipelineDef(ByteBuffer _bb, ModelPipelineDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public ModelPipelineDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The list of assets to import.
+ */
+ public ModelPipelineImportDef sources(int j) { return sources(new ModelPipelineImportDef(), j); }
+ public ModelPipelineImportDef sources(ModelPipelineImportDef obj, int j) { int o = __offset(4); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int sourcesLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * The list of models used for rendering. Each index in the list specifies
+ * an LOD level.
+ */
+ public ModelPipelineRenderableDef renderables(int j) { return renderables(new ModelPipelineRenderableDef(), j); }
+ public ModelPipelineRenderableDef renderables(ModelPipelineRenderableDef obj, int j) { int o = __offset(6); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int renderablesLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * The model used for collision.
+ */
+ public ModelPipelineCollidableDef collidable() { return collidable(new ModelPipelineCollidableDef()); }
+ public ModelPipelineCollidableDef collidable(ModelPipelineCollidableDef obj) { int o = __offset(8); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; }
+ /**
+ * The model used for skeletal animations.
+ */
+ public ModelPipelineSkeletonDef skeleton() { return skeleton(new ModelPipelineSkeletonDef()); }
+ public ModelPipelineSkeletonDef skeleton(ModelPipelineSkeletonDef obj) { int o = __offset(10); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; }
+ /**
+ * The textures to be used by the renderables.
+ */
+ public TextureDef textures(int j) { return textures(new TextureDef(), j); }
+ public TextureDef textures(TextureDef obj, int j) { int o = __offset(12); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int texturesLength() { int o = __offset(12); return o != 0 ? __vector_len(o) : 0; }
+
+ public static int createModelPipelineDef(FlatBufferBuilder builder,
+ int sourcesOffset,
+ int renderablesOffset,
+ int collidableOffset,
+ int skeletonOffset,
+ int texturesOffset) {
+ builder.startObject(5);
+ ModelPipelineDef.addTextures(builder, texturesOffset);
+ ModelPipelineDef.addSkeleton(builder, skeletonOffset);
+ ModelPipelineDef.addCollidable(builder, collidableOffset);
+ ModelPipelineDef.addRenderables(builder, renderablesOffset);
+ ModelPipelineDef.addSources(builder, sourcesOffset);
+ return ModelPipelineDef.endModelPipelineDef(builder);
+ }
+
+ public static void startModelPipelineDef(FlatBufferBuilder builder) { builder.startObject(5); }
+ public static void addSources(FlatBufferBuilder builder, int sourcesOffset) { builder.addOffset(0, sourcesOffset, 0); }
+ public static int createSourcesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startSourcesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addRenderables(FlatBufferBuilder builder, int renderablesOffset) { builder.addOffset(1, renderablesOffset, 0); }
+ public static int createRenderablesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startRenderablesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addCollidable(FlatBufferBuilder builder, int collidableOffset) { builder.addOffset(2, collidableOffset, 0); }
+ public static void addSkeleton(FlatBufferBuilder builder, int skeletonOffset) { builder.addOffset(3, skeletonOffset, 0); }
+ public static void addTextures(FlatBufferBuilder builder, int texturesOffset) { builder.addOffset(4, texturesOffset, 0); }
+ public static int createTexturesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startTexturesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endModelPipelineDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineImportDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineImportDef.java
new file mode 100644
index 0000000..5ccd076
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineImportDef.java
@@ -0,0 +1,162 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Information about how to import an asset into the model_pipeline.
+ */
+public final class ModelPipelineImportDef extends Table {
+ public static ModelPipelineImportDef getRootAsModelPipelineImportDef(ByteBuffer _bb) { return getRootAsModelPipelineImportDef(_bb, new ModelPipelineImportDef()); }
+ public static ModelPipelineImportDef getRootAsModelPipelineImportDef(ByteBuffer _bb, ModelPipelineImportDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public ModelPipelineImportDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The short name of the asset that is referenced by the individual model
+ * components (eg. renderables, collidables, etc.) below.
+ */
+ public String name() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer nameAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer nameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+ /**
+ * The location of disk of the asset.
+ */
+ public String file() { int o = __offset(6); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer fileAsByteBuffer() { return __vector_as_bytebuffer(6, 1); }
+ public ByteBuffer fileInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 1); }
+ /**
+ * Inserts an extra node into the asset hierarchy so that the resulting model
+ * is centered around the origin.
+ */
+ public boolean recenter() { int o = __offset(8); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+ /**
+ * Multiplier applied to the model to change its scale.
+ */
+ public float scale() { int o = __offset(10); return o != 0 ? bb.getFloat(o + bb_pos) : 1.0f; }
+ /**
+ * The axis system used by the model asset.
+ */
+ public int axisSystem() { int o = __offset(12); return o != 0 ? bb.getInt(o + bb_pos) : -1; }
+ /**
+ * The limit angle (in degrees) between two normals being considered for
+ * tangent space smoothing.
+ */
+ public float smoothingAngle() { int o = __offset(14); return o != 0 ? bb.getFloat(o + bb_pos) : 45.0f; }
+ /**
+ * Limit per-vertex bone weights to the N most significant bones.
+ */
+ public int maxBoneWeights() { int o = __offset(16); return o != 0 ? bb.getInt(o + bb_pos) : 4; }
+ /**
+ * some clients do not use LOG to report errors
+ */
+ public boolean reportErrorsToStdout() { int o = __offset(18); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+ /**
+ * Inverts vertical texture coordinates when enabled (D3D/OGL difference).
+ */
+ public boolean flipTextureCoordinates() { int o = __offset(20); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+ /**
+ * Pre-transforms vertices by their node hierarchy so that all vertices are
+ * in the same object-space, and the node hierarchy is flattened.
+ */
+ public boolean flattenHierarchyAndTransformVerticesToRootSpace() { int o = __offset(22); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+ /**
+ * If a model contains both metallic-roughness textures and
+ * specular-glossiness textures, this flag causes the import to only use the
+ * specular-glossiness textures. Otherwise, it uses the metallic-rougness
+ * textures.
+ */
+ public boolean useSpecularGlossinessTexturesIfPresent() { int o = __offset(24); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+ /**
+ * Toggle for Assimp's aiProcess_FixInfacingNormals process.
+ */
+ public boolean fixInfacingNormals() { int o = __offset(26); return o != 0 ? 0!=bb.get(o + bb_pos) : true; }
+ /**
+ * Encodes a sign into the w value of the orientation quaternion such that >0
+ * implies a right handed space, and <0 implies a left handed space. w==0
+ * should never happen. This allows orientation components to be encoded in a
+ */
+ public boolean ensureVertexOrientationWNotZero() { int o = __offset(28); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+ /**
+ * Distinct from 'scale'; defines the unit we expect positions to be in.
+ * Kept as 0 for backwards compatibility, this would be 100.0 for contexts
+ * where world units would be measured in meters, and 2.54 for inches.
+ */
+ public float cmPerUnit() { int o = __offset(30); return o != 0 ? bb.getFloat(o + bb_pos) : 0.0f; }
+ /**
+ * The names of the nodes that contain the target meshes to import from the
+ * asset. If a node has a mesh and its name is present in this list, the mesh
+ * data will be added to the output model. If this list is empty, all meshes
+ * will be added to the output model.
+ */
+ public String targetMeshes(int j) { int o = __offset(32); return o != 0 ? __string(__vector(o) + j * 4) : null; }
+ public int targetMeshesLength() { int o = __offset(32); return o != 0 ? __vector_len(o) : 0; }
+ public boolean mergeMaterials() { int o = __offset(34); return o != 0 ? 0!=bb.get(o + bb_pos) : true; }
+
+ public static int createModelPipelineImportDef(FlatBufferBuilder builder,
+ int nameOffset,
+ int fileOffset,
+ boolean recenter,
+ float scale,
+ int axis_system,
+ float smoothing_angle,
+ int max_bone_weights,
+ boolean report_errors_to_stdout,
+ boolean flip_texture_coordinates,
+ boolean flatten_hierarchy_and_transform_vertices_to_root_space,
+ boolean use_specular_glossiness_textures_if_present,
+ boolean fix_infacing_normals,
+ boolean ensure_vertex_orientation_w_not_zero,
+ float cm_per_unit,
+ int target_meshesOffset,
+ boolean merge_materials) {
+ builder.startObject(16);
+ ModelPipelineImportDef.addTargetMeshes(builder, target_meshesOffset);
+ ModelPipelineImportDef.addCmPerUnit(builder, cm_per_unit);
+ ModelPipelineImportDef.addMaxBoneWeights(builder, max_bone_weights);
+ ModelPipelineImportDef.addSmoothingAngle(builder, smoothing_angle);
+ ModelPipelineImportDef.addAxisSystem(builder, axis_system);
+ ModelPipelineImportDef.addScale(builder, scale);
+ ModelPipelineImportDef.addFile(builder, fileOffset);
+ ModelPipelineImportDef.addName(builder, nameOffset);
+ ModelPipelineImportDef.addMergeMaterials(builder, merge_materials);
+ ModelPipelineImportDef.addEnsureVertexOrientationWNotZero(builder, ensure_vertex_orientation_w_not_zero);
+ ModelPipelineImportDef.addFixInfacingNormals(builder, fix_infacing_normals);
+ ModelPipelineImportDef.addUseSpecularGlossinessTexturesIfPresent(builder, use_specular_glossiness_textures_if_present);
+ ModelPipelineImportDef.addFlattenHierarchyAndTransformVerticesToRootSpace(builder, flatten_hierarchy_and_transform_vertices_to_root_space);
+ ModelPipelineImportDef.addFlipTextureCoordinates(builder, flip_texture_coordinates);
+ ModelPipelineImportDef.addReportErrorsToStdout(builder, report_errors_to_stdout);
+ ModelPipelineImportDef.addRecenter(builder, recenter);
+ return ModelPipelineImportDef.endModelPipelineImportDef(builder);
+ }
+
+ public static void startModelPipelineImportDef(FlatBufferBuilder builder) { builder.startObject(16); }
+ public static void addName(FlatBufferBuilder builder, int nameOffset) { builder.addOffset(0, nameOffset, 0); }
+ public static void addFile(FlatBufferBuilder builder, int fileOffset) { builder.addOffset(1, fileOffset, 0); }
+ public static void addRecenter(FlatBufferBuilder builder, boolean recenter) { builder.addBoolean(2, recenter, false); }
+ public static void addScale(FlatBufferBuilder builder, float scale) { builder.addFloat(3, scale, 1.0f); }
+ public static void addAxisSystem(FlatBufferBuilder builder, int axisSystem) { builder.addInt(4, axisSystem, -1); }
+ public static void addSmoothingAngle(FlatBufferBuilder builder, float smoothingAngle) { builder.addFloat(5, smoothingAngle, 45.0f); }
+ public static void addMaxBoneWeights(FlatBufferBuilder builder, int maxBoneWeights) { builder.addInt(6, maxBoneWeights, 4); }
+ public static void addReportErrorsToStdout(FlatBufferBuilder builder, boolean reportErrorsToStdout) { builder.addBoolean(7, reportErrorsToStdout, false); }
+ public static void addFlipTextureCoordinates(FlatBufferBuilder builder, boolean flipTextureCoordinates) { builder.addBoolean(8, flipTextureCoordinates, false); }
+ public static void addFlattenHierarchyAndTransformVerticesToRootSpace(FlatBufferBuilder builder, boolean flattenHierarchyAndTransformVerticesToRootSpace) { builder.addBoolean(9, flattenHierarchyAndTransformVerticesToRootSpace, false); }
+ public static void addUseSpecularGlossinessTexturesIfPresent(FlatBufferBuilder builder, boolean useSpecularGlossinessTexturesIfPresent) { builder.addBoolean(10, useSpecularGlossinessTexturesIfPresent, false); }
+ public static void addFixInfacingNormals(FlatBufferBuilder builder, boolean fixInfacingNormals) { builder.addBoolean(11, fixInfacingNormals, true); }
+ public static void addEnsureVertexOrientationWNotZero(FlatBufferBuilder builder, boolean ensureVertexOrientationWNotZero) { builder.addBoolean(12, ensureVertexOrientationWNotZero, false); }
+ public static void addCmPerUnit(FlatBufferBuilder builder, float cmPerUnit) { builder.addFloat(13, cmPerUnit, 0.0f); }
+ public static void addTargetMeshes(FlatBufferBuilder builder, int targetMeshesOffset) { builder.addOffset(14, targetMeshesOffset, 0); }
+ public static int createTargetMeshesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startTargetMeshesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addMergeMaterials(FlatBufferBuilder builder, boolean mergeMaterials) { builder.addBoolean(15, mergeMaterials, true); }
+ public static int endModelPipelineImportDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineMaterialDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineMaterialDef.java
new file mode 100644
index 0000000..3adc132
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineMaterialDef.java
@@ -0,0 +1,49 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Information about a renderable material.
+ */
+public final class ModelPipelineMaterialDef extends Table {
+ public static ModelPipelineMaterialDef getRootAsModelPipelineMaterialDef(ByteBuffer _bb) { return getRootAsModelPipelineMaterialDef(_bb, new ModelPipelineMaterialDef()); }
+ public static ModelPipelineMaterialDef getRootAsModelPipelineMaterialDef(ByteBuffer _bb, ModelPipelineMaterialDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public ModelPipelineMaterialDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * Description of the desired material.
+ */
+ public MaterialDef material() { return material(new MaterialDef()); }
+ public MaterialDef material(MaterialDef obj) { int o = __offset(4); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; }
+ /**
+ * Optional replacement name to use at runtime.
+ */
+ public String nameOverride() { int o = __offset(6); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer nameOverrideAsByteBuffer() { return __vector_as_bytebuffer(6, 1); }
+ public ByteBuffer nameOverrideInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 1); }
+
+ public static int createModelPipelineMaterialDef(FlatBufferBuilder builder,
+ int materialOffset,
+ int name_overrideOffset) {
+ builder.startObject(2);
+ ModelPipelineMaterialDef.addNameOverride(builder, name_overrideOffset);
+ ModelPipelineMaterialDef.addMaterial(builder, materialOffset);
+ return ModelPipelineMaterialDef.endModelPipelineMaterialDef(builder);
+ }
+
+ public static void startModelPipelineMaterialDef(FlatBufferBuilder builder) { builder.startObject(2); }
+ public static void addMaterial(FlatBufferBuilder builder, int materialOffset) { builder.addOffset(0, materialOffset, 0); }
+ public static void addNameOverride(FlatBufferBuilder builder, int nameOverrideOffset) { builder.addOffset(1, nameOverrideOffset, 0); }
+ public static int endModelPipelineMaterialDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineRenderableDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineRenderableDef.java
new file mode 100644
index 0000000..0b4e723
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineRenderableDef.java
@@ -0,0 +1,67 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Information about a renderable model.
+ */
+public final class ModelPipelineRenderableDef extends Table {
+ public static ModelPipelineRenderableDef getRootAsModelPipelineRenderableDef(ByteBuffer _bb) { return getRootAsModelPipelineRenderableDef(_bb, new ModelPipelineRenderableDef()); }
+ public static ModelPipelineRenderableDef getRootAsModelPipelineRenderableDef(ByteBuffer _bb, ModelPipelineRenderableDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public ModelPipelineRenderableDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The name of ModelPipelineImportDef defining this model.
+ */
+ public String source() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer sourceAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer sourceInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+ /**
+ * The material properties for the model. Each index in the array corresponds
+ * to a submesh in the model.
+ */
+ public ModelPipelineMaterialDef materials(int j) { return materials(new ModelPipelineMaterialDef(), j); }
+ public ModelPipelineMaterialDef materials(ModelPipelineMaterialDef obj, int j) { int o = __offset(6); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int materialsLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * Specific vertex attributes to export. For multiple attributes with the
+ * same usage (eg. two uv-coords for textures), simply list the attribute
+ * twice, regardless of order.
+ */
+ public int attributes(int j) { int o = __offset(8); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; }
+ public int attributesLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer attributesAsByteBuffer() { return __vector_as_bytebuffer(8, 4); }
+ public ByteBuffer attributesInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 4); }
+
+ public static int createModelPipelineRenderableDef(FlatBufferBuilder builder,
+ int sourceOffset,
+ int materialsOffset,
+ int attributesOffset) {
+ builder.startObject(3);
+ ModelPipelineRenderableDef.addAttributes(builder, attributesOffset);
+ ModelPipelineRenderableDef.addMaterials(builder, materialsOffset);
+ ModelPipelineRenderableDef.addSource(builder, sourceOffset);
+ return ModelPipelineRenderableDef.endModelPipelineRenderableDef(builder);
+ }
+
+ public static void startModelPipelineRenderableDef(FlatBufferBuilder builder) { builder.startObject(3); }
+ public static void addSource(FlatBufferBuilder builder, int sourceOffset) { builder.addOffset(0, sourceOffset, 0); }
+ public static void addMaterials(FlatBufferBuilder builder, int materialsOffset) { builder.addOffset(1, materialsOffset, 0); }
+ public static int createMaterialsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startMaterialsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addAttributes(FlatBufferBuilder builder, int attributesOffset) { builder.addOffset(2, attributesOffset, 0); }
+ public static int createAttributesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); }
+ public static void startAttributesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endModelPipelineRenderableDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineSkeletonDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineSkeletonDef.java
new file mode 100644
index 0000000..34b1603
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/ModelPipelineSkeletonDef.java
@@ -0,0 +1,41 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Information about an animatable model.
+ */
+public final class ModelPipelineSkeletonDef extends Table {
+ public static ModelPipelineSkeletonDef getRootAsModelPipelineSkeletonDef(ByteBuffer _bb) { return getRootAsModelPipelineSkeletonDef(_bb, new ModelPipelineSkeletonDef()); }
+ public static ModelPipelineSkeletonDef getRootAsModelPipelineSkeletonDef(ByteBuffer _bb, ModelPipelineSkeletonDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public ModelPipelineSkeletonDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The name of ModelPipelineImportDef defining this model.
+ */
+ public String source() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer sourceAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer sourceInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+
+ public static int createModelPipelineSkeletonDef(FlatBufferBuilder builder,
+ int sourceOffset) {
+ builder.startObject(1);
+ ModelPipelineSkeletonDef.addSource(builder, sourceOffset);
+ return ModelPipelineSkeletonDef.endModelPipelineSkeletonDef(builder);
+ }
+
+ public static void startModelPipelineSkeletonDef(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addSource(FlatBufferBuilder builder, int sourceOffset) { builder.addOffset(0, sourceOffset, 0); }
+ public static int endModelPipelineSkeletonDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/OptionalBool.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/OptionalBool.java
new file mode 100644
index 0000000..2a3a04d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/OptionalBool.java
@@ -0,0 +1,24 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+public final class OptionalBool {
+ private OptionalBool() { }
+ /**
+ * Don't force the value on entity create.
+ */
+ public static final int Unset = 0;
+ /**
+ * Force the value to true when created.
+ */
+ public static final int True = 1;
+ /**
+ * Force the value to false when created.
+ */
+ public static final int False = 2;
+
+ public static final String[] names = { "Unset", "True", "False", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Quat.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Quat.java
new file mode 100644
index 0000000..176aec4
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Quat.java
@@ -0,0 +1,29 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Quat extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public Quat __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public float x() { return bb.getFloat(bb_pos + 0); }
+ public float y() { return bb.getFloat(bb_pos + 4); }
+ public float z() { return bb.getFloat(bb_pos + 8); }
+ public float w() { return bb.getFloat(bb_pos + 12); }
+
+ public static int createQuat(FlatBufferBuilder builder, float x, float y, float z, float w) {
+ builder.prep(4, 16);
+ builder.putFloat(w);
+ builder.putFloat(z);
+ builder.putFloat(y);
+ builder.putFloat(x);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Rect.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Rect.java
new file mode 100644
index 0000000..a5306ed
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Rect.java
@@ -0,0 +1,29 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Rect extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public Rect __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public float x() { return bb.getFloat(bb_pos + 0); }
+ public float y() { return bb.getFloat(bb_pos + 4); }
+ public float w() { return bb.getFloat(bb_pos + 8); }
+ public float h() { return bb.getFloat(bb_pos + 12); }
+
+ public static int createRect(FlatBufferBuilder builder, float x, float y, float w, float h) {
+ builder.prep(4, 16);
+ builder.putFloat(h);
+ builder.putFloat(w);
+ builder.putFloat(y);
+ builder.putFloat(x);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Recti.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Recti.java
new file mode 100644
index 0000000..31b5994
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Recti.java
@@ -0,0 +1,29 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Recti extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public Recti __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public int x() { return bb.getInt(bb_pos + 0); }
+ public int y() { return bb.getInt(bb_pos + 4); }
+ public int w() { return bb.getInt(bb_pos + 8); }
+ public int h() { return bb.getInt(bb_pos + 12); }
+
+ public static int createRecti(FlatBufferBuilder builder, int x, int y, int w, int h) {
+ builder.prep(4, 16);
+ builder.putInt(h);
+ builder.putInt(w);
+ builder.putInt(y);
+ builder.putInt(x);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/SkeletonDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/SkeletonDef.java
new file mode 100644
index 0000000..d29360a
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/SkeletonDef.java
@@ -0,0 +1,69 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Describes the skeleton used by the RigSystem to handle skinned animations.
+ */
+public final class SkeletonDef extends Table {
+ public static SkeletonDef getRootAsSkeletonDef(ByteBuffer _bb) { return getRootAsSkeletonDef(_bb, new SkeletonDef()); }
+ public static SkeletonDef getRootAsSkeletonDef(ByteBuffer _bb, SkeletonDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public SkeletonDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ /**
+ * The names of each bone in the skeleton. Each bone in the skeleton can
+ * be uniquely identified by an index into this array.
+ */
+ public String boneNames(int j) { int o = __offset(4); return o != 0 ? __string(__vector(o) + j * 4) : null; }
+ public int boneNamesLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; }
+ /**
+ * Effectively a map of a bone to its parent bone.
+ */
+ public int boneParents(int j) { int o = __offset(6); return o != 0 ? bb.get(__vector(o) + j * 1) & 0xFF : 0; }
+ public int boneParentsLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer boneParentsAsByteBuffer() { return __vector_as_bytebuffer(6, 1); }
+ public ByteBuffer boneParentsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 1); }
+ /**
+ * For information on how the matrices below are used for skinning, see
+ * RigSystem::UpdateShaderTransforms().
+ * The "inverse bind matrices" for each bone. Transforms from mesh space to
+ * bone space so that skinning may be applied.
+ */
+ public Mat4x3 boneTransforms(int j) { return boneTransforms(new Mat4x3(), j); }
+ public Mat4x3 boneTransforms(Mat4x3 obj, int j) { int o = __offset(8); return o != 0 ? obj.__assign(__vector(o) + j * 48, bb) : null; }
+ public int boneTransformsLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; }
+
+ public static int createSkeletonDef(FlatBufferBuilder builder,
+ int bone_namesOffset,
+ int bone_parentsOffset,
+ int bone_transformsOffset) {
+ builder.startObject(5);
+ SkeletonDef.addBoneTransforms(builder, bone_transformsOffset);
+ SkeletonDef.addBoneParents(builder, bone_parentsOffset);
+ SkeletonDef.addBoneNames(builder, bone_namesOffset);
+ return SkeletonDef.endSkeletonDef(builder);
+ }
+
+ public static void startSkeletonDef(FlatBufferBuilder builder) { builder.startObject(5); }
+ public static void addBoneNames(FlatBufferBuilder builder, int boneNamesOffset) { builder.addOffset(0, boneNamesOffset, 0); }
+ public static int createBoneNamesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startBoneNamesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addBoneParents(FlatBufferBuilder builder, int boneParentsOffset) { builder.addOffset(1, boneParentsOffset, 0); }
+ public static int createBoneParentsVector(FlatBufferBuilder builder, byte[] data) { return builder.createByteVector(data); }
+ public static int createBoneParentsVector(FlatBufferBuilder builder, ByteBuffer data) { return builder.createByteVector(data); }
+ public static void startBoneParentsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(1, numElems, 1); }
+ public static void addBoneTransforms(FlatBufferBuilder builder, int boneTransformsOffset) { builder.addOffset(2, boneTransformsOffset, 0); }
+ public static void startBoneTransformsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(48, numElems, 4); }
+ public static int endSkeletonDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/SubmeshAabb.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/SubmeshAabb.java
new file mode 100644
index 0000000..1f987df
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/SubmeshAabb.java
@@ -0,0 +1,33 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * A table to hold a submesh aabb.
+ */
+public final class SubmeshAabb extends Table {
+ public static SubmeshAabb getRootAsSubmeshAabb(ByteBuffer _bb) { return getRootAsSubmeshAabb(_bb, new SubmeshAabb()); }
+ public static SubmeshAabb getRootAsSubmeshAabb(ByteBuffer _bb, SubmeshAabb obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public SubmeshAabb __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public Vec3 minPosition() { return minPosition(new Vec3()); }
+ public Vec3 minPosition(Vec3 obj) { int o = __offset(4); return o != 0 ? obj.__assign(o + bb_pos, bb) : null; }
+ public Vec3 maxPosition() { return maxPosition(new Vec3()); }
+ public Vec3 maxPosition(Vec3 obj) { int o = __offset(6); return o != 0 ? obj.__assign(o + bb_pos, bb) : null; }
+
+ public static void startSubmeshAabb(FlatBufferBuilder builder) { builder.startObject(2); }
+ public static void addMinPosition(FlatBufferBuilder builder, int minPositionOffset) { builder.addStruct(0, minPositionOffset, 0); }
+ public static void addMaxPosition(FlatBufferBuilder builder, int maxPositionOffset) { builder.addStruct(1, maxPositionOffset, 0); }
+ public static int endSubmeshAabb(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureDef.java
new file mode 100644
index 0000000..f64f9e7
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureDef.java
@@ -0,0 +1,87 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class TextureDef extends Table {
+ public static TextureDef getRootAsTextureDef(ByteBuffer _bb) { return getRootAsTextureDef(_bb, new TextureDef()); }
+ public static TextureDef getRootAsTextureDef(ByteBuffer _bb, TextureDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public TextureDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public String name() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer nameAsByteBuffer() { return __vector_as_bytebuffer(4, 1); }
+ public ByteBuffer nameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); }
+ public String file() { int o = __offset(6); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer fileAsByteBuffer() { return __vector_as_bytebuffer(6, 1); }
+ public ByteBuffer fileInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 1); }
+ public int data(int j) { int o = __offset(8); return o != 0 ? bb.get(__vector(o) + j * 1) & 0xFF : 0; }
+ public int dataLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; }
+ public ByteBuffer dataAsByteBuffer() { return __vector_as_bytebuffer(8, 1); }
+ public ByteBuffer dataInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 1); }
+ public boolean generateMipmaps() { int o = __offset(10); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+ public boolean premultiplyAlpha() { int o = __offset(12); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+ public int minFilter() { int o = __offset(14); return o != 0 ? bb.getShort(o + bb_pos) & 0xFFFF : 4; }
+ public int magFilter() { int o = __offset(16); return o != 0 ? bb.getShort(o + bb_pos) & 0xFFFF : 1; }
+ public int wrapS() { int o = __offset(18); return o != 0 ? bb.getShort(o + bb_pos) & 0xFFFF : 4; }
+ public int wrapT() { int o = __offset(20); return o != 0 ? bb.getShort(o + bb_pos) & 0xFFFF : 4; }
+ public int wrapR() { int o = __offset(22); return o != 0 ? bb.getShort(o + bb_pos) & 0xFFFF : 4; }
+ public int targetType() { int o = __offset(24); return o != 0 ? bb.getShort(o + bb_pos) & 0xFFFF : 0; }
+ public boolean isRgbm() { int o = __offset(26); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+
+ public static int createTextureDef(FlatBufferBuilder builder,
+ int nameOffset,
+ int fileOffset,
+ int dataOffset,
+ boolean generate_mipmaps,
+ boolean premultiply_alpha,
+ int min_filter,
+ int mag_filter,
+ int wrap_s,
+ int wrap_t,
+ int wrap_r,
+ int target_type,
+ boolean is_rgbm) {
+ builder.startObject(12);
+ TextureDef.addData(builder, dataOffset);
+ TextureDef.addFile(builder, fileOffset);
+ TextureDef.addName(builder, nameOffset);
+ TextureDef.addTargetType(builder, target_type);
+ TextureDef.addWrapR(builder, wrap_r);
+ TextureDef.addWrapT(builder, wrap_t);
+ TextureDef.addWrapS(builder, wrap_s);
+ TextureDef.addMagFilter(builder, mag_filter);
+ TextureDef.addMinFilter(builder, min_filter);
+ TextureDef.addIsRgbm(builder, is_rgbm);
+ TextureDef.addPremultiplyAlpha(builder, premultiply_alpha);
+ TextureDef.addGenerateMipmaps(builder, generate_mipmaps);
+ return TextureDef.endTextureDef(builder);
+ }
+
+ public static void startTextureDef(FlatBufferBuilder builder) { builder.startObject(12); }
+ public static void addName(FlatBufferBuilder builder, int nameOffset) { builder.addOffset(0, nameOffset, 0); }
+ public static void addFile(FlatBufferBuilder builder, int fileOffset) { builder.addOffset(1, fileOffset, 0); }
+ public static void addData(FlatBufferBuilder builder, int dataOffset) { builder.addOffset(2, dataOffset, 0); }
+ public static int createDataVector(FlatBufferBuilder builder, byte[] data) { return builder.createByteVector(data); }
+ public static int createDataVector(FlatBufferBuilder builder, ByteBuffer data) { return builder.createByteVector(data); }
+ public static void startDataVector(FlatBufferBuilder builder, int numElems) { builder.startVector(1, numElems, 1); }
+ public static void addGenerateMipmaps(FlatBufferBuilder builder, boolean generateMipmaps) { builder.addBoolean(3, generateMipmaps, false); }
+ public static void addPremultiplyAlpha(FlatBufferBuilder builder, boolean premultiplyAlpha) { builder.addBoolean(4, premultiplyAlpha, false); }
+ public static void addMinFilter(FlatBufferBuilder builder, int minFilter) { builder.addShort(5, (short)minFilter, (short)4); }
+ public static void addMagFilter(FlatBufferBuilder builder, int magFilter) { builder.addShort(6, (short)magFilter, (short)1); }
+ public static void addWrapS(FlatBufferBuilder builder, int wrapS) { builder.addShort(7, (short)wrapS, (short)4); }
+ public static void addWrapT(FlatBufferBuilder builder, int wrapT) { builder.addShort(8, (short)wrapT, (short)4); }
+ public static void addWrapR(FlatBufferBuilder builder, int wrapR) { builder.addShort(9, (short)wrapR, (short)4); }
+ public static void addTargetType(FlatBufferBuilder builder, int targetType) { builder.addShort(10, (short)targetType, (short)0); }
+ public static void addIsRgbm(FlatBufferBuilder builder, boolean isRgbm) { builder.addBoolean(11, isRgbm, false); }
+ public static int endTextureDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureFiltering.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureFiltering.java
new file mode 100644
index 0000000..c7222b0
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureFiltering.java
@@ -0,0 +1,18 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+public final class TextureFiltering {
+ private TextureFiltering() { }
+ public static final short Nearest = 0;
+ public static final short Linear = 1;
+ public static final short NearestMipmapNearest = 2;
+ public static final short LinearMipmapNearest = 3;
+ public static final short NearestMipmapLinear = 4;
+ public static final short LinearMipmapLinear = 5;
+
+ public static final String[] names = { "Nearest", "Linear", "NearestMipmapNearest", "LinearMipmapNearest", "NearestMipmapLinear", "LinearMipmapLinear", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureTargetType.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureTargetType.java
new file mode 100644
index 0000000..49b7b53
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureTargetType.java
@@ -0,0 +1,14 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+public final class TextureTargetType {
+ private TextureTargetType() { }
+ public static final short Standard2d = 0;
+ public static final short CubeMap = 1;
+
+ public static final String[] names = { "Standard2d", "CubeMap", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureWrap.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureWrap.java
new file mode 100644
index 0000000..6adcc9c
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/TextureWrap.java
@@ -0,0 +1,17 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+public final class TextureWrap {
+ private TextureWrap() { }
+ public static final short ClampToBorder = 0;
+ public static final short ClampToEdge = 1;
+ public static final short MirroredRepeat = 2;
+ public static final short MirrorClampToEdge = 3;
+ public static final short Repeat = 4;
+
+ public static final String[] names = { "ClampToBorder", "ClampToEdge", "MirroredRepeat", "MirrorClampToEdge", "Repeat", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantArrayDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantArrayDef.java
new file mode 100644
index 0000000..33c1373
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantArrayDef.java
@@ -0,0 +1,40 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * The flatbuffer equivalent for lull::VariantArray.
+ */
+public final class VariantArrayDef extends Table {
+ public static VariantArrayDef getRootAsVariantArrayDef(ByteBuffer _bb) { return getRootAsVariantArrayDef(_bb, new VariantArrayDef()); }
+ public static VariantArrayDef getRootAsVariantArrayDef(ByteBuffer _bb, VariantArrayDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public VariantArrayDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public VariantArrayDefImpl values(int j) { return values(new VariantArrayDefImpl(), j); }
+ public VariantArrayDefImpl values(VariantArrayDefImpl obj, int j) { int o = __offset(4); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int valuesLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; }
+
+ public static int createVariantArrayDef(FlatBufferBuilder builder,
+ int valuesOffset) {
+ builder.startObject(1);
+ VariantArrayDef.addValues(builder, valuesOffset);
+ return VariantArrayDef.endVariantArrayDef(builder);
+ }
+
+ public static void startVariantArrayDef(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValues(FlatBufferBuilder builder, int valuesOffset) { builder.addOffset(0, valuesOffset, 0); }
+ public static int createValuesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startValuesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endVariantArrayDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantArrayDefImpl.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantArrayDefImpl.java
new file mode 100644
index 0000000..ec7e332
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantArrayDefImpl.java
@@ -0,0 +1,41 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Internal table for VariantArrayDef. Unions need to be wrapped in a table
+ * in order to have an array of them.
+ */
+public final class VariantArrayDefImpl extends Table {
+ public static VariantArrayDefImpl getRootAsVariantArrayDefImpl(ByteBuffer _bb) { return getRootAsVariantArrayDefImpl(_bb, new VariantArrayDefImpl()); }
+ public static VariantArrayDefImpl getRootAsVariantArrayDefImpl(ByteBuffer _bb, VariantArrayDefImpl obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public VariantArrayDefImpl __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public byte valueType() { int o = __offset(4); return o != 0 ? bb.get(o + bb_pos) : 0; }
+ public Table value(Table obj) { int o = __offset(6); return o != 0 ? __union(obj, o) : null; }
+
+ public static int createVariantArrayDefImpl(FlatBufferBuilder builder,
+ byte value_type,
+ int valueOffset) {
+ builder.startObject(2);
+ VariantArrayDefImpl.addValue(builder, valueOffset);
+ VariantArrayDefImpl.addValueType(builder, value_type);
+ return VariantArrayDefImpl.endVariantArrayDefImpl(builder);
+ }
+
+ public static void startVariantArrayDefImpl(FlatBufferBuilder builder) { builder.startObject(2); }
+ public static void addValueType(FlatBufferBuilder builder, byte valueType) { builder.addByte(0, valueType, 0); }
+ public static void addValue(FlatBufferBuilder builder, int valueOffset) { builder.addOffset(1, valueOffset, 0); }
+ public static int endVariantArrayDefImpl(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantDef.java
new file mode 100644
index 0000000..674472f
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantDef.java
@@ -0,0 +1,28 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+/**
+ * A variant type that can be converted into a lull::Variant.
+ */
+public final class VariantDef {
+ private VariantDef() { }
+ public static final byte NONE = 0;
+ public static final byte DataBool = 1;
+ public static final byte DataInt = 2;
+ public static final byte DataFloat = 3;
+ public static final byte DataString = 4;
+ public static final byte DataHashValue = 5;
+ public static final byte DataVec2 = 6;
+ public static final byte DataVec3 = 7;
+ public static final byte DataVec4 = 8;
+ public static final byte DataQuat = 9;
+ public static final byte DataBytes = 10;
+ public static final byte VariantArrayDef = 11;
+ public static final byte VariantMapDef = 12;
+
+ public static final String[] names = { "NONE", "DataBool", "DataInt", "DataFloat", "DataString", "DataHashValue", "DataVec2", "DataVec3", "DataVec4", "DataQuat", "DataBytes", "VariantArrayDef", "VariantMapDef", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantMapDef.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantMapDef.java
new file mode 100644
index 0000000..325828b
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VariantMapDef.java
@@ -0,0 +1,40 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * The flatbuffer equivalent for lull::VariantMap.
+ */
+public final class VariantMapDef extends Table {
+ public static VariantMapDef getRootAsVariantMapDef(ByteBuffer _bb) { return getRootAsVariantMapDef(_bb, new VariantMapDef()); }
+ public static VariantMapDef getRootAsVariantMapDef(ByteBuffer _bb, VariantMapDef obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); }
+ public VariantMapDef __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public KeyVariantPairDef values(int j) { return values(new KeyVariantPairDef(), j); }
+ public KeyVariantPairDef values(KeyVariantPairDef obj, int j) { int o = __offset(4); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int valuesLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; }
+
+ public static int createVariantMapDef(FlatBufferBuilder builder,
+ int valuesOffset) {
+ builder.startObject(1);
+ VariantMapDef.addValues(builder, valuesOffset);
+ return VariantMapDef.endVariantMapDef(builder);
+ }
+
+ public static void startVariantMapDef(FlatBufferBuilder builder) { builder.startObject(1); }
+ public static void addValues(FlatBufferBuilder builder, int valuesOffset) { builder.addOffset(0, valuesOffset, 0); }
+ public static int createValuesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startValuesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endVariantMapDef(FlatBufferBuilder builder) {
+ int o = builder.endObject();
+ return o;
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec2.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec2.java
new file mode 100644
index 0000000..75c89af
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec2.java
@@ -0,0 +1,25 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Vec2 extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public Vec2 __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public float x() { return bb.getFloat(bb_pos + 0); }
+ public float y() { return bb.getFloat(bb_pos + 4); }
+
+ public static int createVec2(FlatBufferBuilder builder, float x, float y) {
+ builder.prep(4, 8);
+ builder.putFloat(y);
+ builder.putFloat(x);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec2i.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec2i.java
new file mode 100644
index 0000000..a91ccf8
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec2i.java
@@ -0,0 +1,25 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Vec2i extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public Vec2i __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public int x() { return bb.getInt(bb_pos + 0); }
+ public int y() { return bb.getInt(bb_pos + 4); }
+
+ public static int createVec2i(FlatBufferBuilder builder, int x, int y) {
+ builder.prep(4, 8);
+ builder.putInt(y);
+ builder.putInt(x);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec3.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec3.java
new file mode 100644
index 0000000..460ade4
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec3.java
@@ -0,0 +1,27 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Vec3 extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public Vec3 __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public float x() { return bb.getFloat(bb_pos + 0); }
+ public float y() { return bb.getFloat(bb_pos + 4); }
+ public float z() { return bb.getFloat(bb_pos + 8); }
+
+ public static int createVec3(FlatBufferBuilder builder, float x, float y, float z) {
+ builder.prep(4, 12);
+ builder.putFloat(z);
+ builder.putFloat(y);
+ builder.putFloat(x);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec4.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec4.java
new file mode 100644
index 0000000..3403151
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/Vec4.java
@@ -0,0 +1,29 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Vec4 extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public Vec4 __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public float x() { return bb.getFloat(bb_pos + 0); }
+ public float y() { return bb.getFloat(bb_pos + 4); }
+ public float z() { return bb.getFloat(bb_pos + 8); }
+ public float w() { return bb.getFloat(bb_pos + 12); }
+
+ public static int createVec4(FlatBufferBuilder builder, float x, float y, float z, float w) {
+ builder.prep(4, 16);
+ builder.putFloat(w);
+ builder.putFloat(z);
+ builder.putFloat(y);
+ builder.putFloat(x);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VertexAttribute.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VertexAttribute.java
new file mode 100644
index 0000000..154b76d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VertexAttribute.java
@@ -0,0 +1,28 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+import java.nio.*;
+
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+/**
+ * Describes a single attribute in vertex format.
+ */
+public final class VertexAttribute extends Struct {
+ public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; }
+ public VertexAttribute __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public int usage() { return bb.getInt(bb_pos + 0); }
+ public int type() { return bb.getInt(bb_pos + 4); }
+
+ public static int createVertexAttribute(FlatBufferBuilder builder, int usage, int type) {
+ builder.prep(4, 8);
+ builder.putInt(type);
+ builder.putInt(usage);
+ return builder.offset();
+ }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VertexAttributeType.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VertexAttributeType.java
new file mode 100644
index 0000000..c8fb5cb
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VertexAttributeType.java
@@ -0,0 +1,24 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+/**
+ * Defines the data structure of a single attribute in a vertex. Each type is
+ * assumed to be 4-byte aligned.
+ */
+public final class VertexAttributeType {
+ private VertexAttributeType() { }
+ public static final int Empty = 0;
+ public static final int Scalar1f = 1;
+ public static final int Vec2f = 2;
+ public static final int Vec3f = 3;
+ public static final int Vec4f = 4;
+ public static final int Vec2us = 5;
+ public static final int Vec4us = 6;
+ public static final int Vec4ub = 7;
+
+ public static final String[] names = { "Empty", "Scalar1f", "Vec2f", "Vec3f", "Vec4f", "Vec2us", "Vec4us", "Vec4ub", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VertexAttributeUsage.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VertexAttributeUsage.java
new file mode 100644
index 0000000..d17db71
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/lullmodel/VertexAttributeUsage.java
@@ -0,0 +1,24 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.google.ar.sceneform.lullmodel;
+
+/**
+ * Defines how data in a vertex is interpreted by the shader.
+ */
+public final class VertexAttributeUsage {
+ private VertexAttributeUsage() { }
+ public static final int Invalid = 0;
+ public static final int Position = 1;
+ public static final int Color = 2;
+ public static final int TexCoord = 3;
+ public static final int Normal = 4;
+ public static final int Tangent = 5;
+ public static final int Orientation = 6;
+ public static final int BoneIndices = 7;
+ public static final int BoneWeights = 8;
+
+ public static final String[] names = { "Invalid", "Position", "Color", "TexCoord", "Normal", "Tangent", "Orientation", "BoneIndices", "BoneWeights", };
+
+ public static String name(int e) { return names[e]; }
+}
+
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/MathHelper.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/MathHelper.java
new file mode 100644
index 0000000..1adb5f0
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/MathHelper.java
@@ -0,0 +1,53 @@
+package com.google.ar.sceneform.math;
+
+/** Static functions for common math operations. */
+public class MathHelper {
+
+ static final float FLT_EPSILON = 1.19209290E-07f;
+ static final float MAX_DELTA = 1.0E-10f;
+
+ /**
+ * Returns true if two floats are equal within a tolerance. Useful for comparing floating point
+ * numbers while accounting for the limitations in floating point precision.
+ */
+ // https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
+ public static boolean almostEqualRelativeAndAbs(float a, float b) {
+ // Check if the numbers are really close -- needed
+ // when comparing numbers near zero.
+ float diff = Math.abs(a - b);
+ if (diff <= MAX_DELTA) {
+ return true;
+ }
+
+ a = Math.abs(a);
+ b = Math.abs(b);
+ float largest = Math.max(a, b);
+
+ if (diff <= largest * FLT_EPSILON) {
+ return true;
+ }
+ return false;
+ }
+
+ /** Clamps a value between a minimum and maximum range. */
+ public static float clamp(float value, float min, float max) {
+ return Math.min(max, Math.max(min, value));
+ }
+
+ /** Clamps a value between a range of 0 and 1. */
+ static float clamp01(float value) {
+ return clamp(value, 0.0f, 1.0f);
+ }
+
+ /**
+ * Linearly interpolates between a and b by a ratio.
+ *
+ * @param a the beginning value
+ * @param b the ending value
+ * @param t ratio between the two floats
+ * @return interpolated value between the two floats
+ */
+ public static float lerp(float a, float b, float t) {
+ return a + t * (b - a);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Matrix.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Matrix.java
new file mode 100644
index 0000000..41a038a
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Matrix.java
@@ -0,0 +1,555 @@
+package com.google.ar.sceneform.math;
+
+import android.util.Log;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * 4x4 Matrix representing translation, scale, and rotation. Column major, right handed [0, 4, 8,
+ * 12] [1, 5, 9, 13] [2, 6, 10, 14] [3, 7, 11, 15]
+ *
+ * @hide
+ */
+// TODO: Evaluate consolidating internal math.
+public class Matrix {
+ private static final String TAG = Matrix.class.getSimpleName();
+
+ public static final float[] IDENTITY_DATA =
+ new float[] {
+ 1.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f
+ };
+
+ public float[] data = new float[16];
+
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Matrix() {
+ set(IDENTITY_DATA);
+ }
+
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Matrix(float[] data) {
+ set(data);
+ }
+
+ public void set(float[] data) {
+ if (data == null || data.length != 16) {
+ Log.w(TAG, "Cannot set Matrix, invalid data.");
+ return;
+ }
+
+ for (int i = 0; i < data.length; i++) {
+ this.data[i] = data[i];
+ }
+ }
+
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public void set(Matrix m) {
+ Preconditions.checkNotNull(m, "Parameter \"m\" was null.");
+ set(m.data);
+ }
+
+ public void decomposeTranslation(Vector3 destTranslation) {
+ destTranslation.x = data[12];
+ destTranslation.y = data[13];
+ destTranslation.z = data[14];
+ }
+
+ public void decomposeScale(Vector3 destScale) {
+ Vector3 temp = new Vector3(data[0], data[1], data[2]);
+ destScale.x = temp.length();
+ temp.set(data[4], data[5], data[6]);
+ destScale.y = temp.length();
+ temp.set(data[8], data[9], data[10]);
+ destScale.z = temp.length();
+ }
+
+ public void decomposeRotation(Vector3 decomposedScale, Quaternion destRotation) {
+ float m00 = data[0];
+ float m01 = data[1];
+ float m02 = data[2];
+ float m03 = data[3];
+ float m10 = data[4];
+ float m11 = data[5];
+ float m12 = data[6];
+ float m13 = data[7];
+ float m20 = data[8];
+ float m21 = data[9];
+ float m22 = data[10];
+ float m23 = data[11];
+ float m30 = data[12];
+ float m31 = data[13];
+ float m32 = data[14];
+ float m33 = data[15];
+
+ // To extract the quaternion, we first remove the scale from the matrix. This is done in-place,
+ // and then after the quaternion is extracted the matrix is set back to it's original values.
+ // This allows us to decompose the rotation without allocating an additional matrix to hold the
+ // rotation matrix, which is better for performance.
+ decomposeRotation(decomposedScale, this);
+ extractQuaternion(destRotation);
+
+ data[0] = m00;
+ data[1] = m01;
+ data[2] = m02;
+ data[3] = m03;
+ data[4] = m10;
+ data[5] = m11;
+ data[6] = m12;
+ data[7] = m13;
+ data[8] = m20;
+ data[9] = m21;
+ data[10] = m22;
+ data[11] = m23;
+ data[12] = m30;
+ data[13] = m31;
+ data[14] = m32;
+ data[15] = m33;
+ }
+
+ public void decomposeRotation(Vector3 decomposedScale, Matrix destMatrix) {
+ // Remove x scale.
+ if (decomposedScale.x != 0.0f) {
+ for (int i = 0; i < 3; i++) {
+ destMatrix.data[i] = data[i] / decomposedScale.x;
+ }
+ }
+
+ destMatrix.data[3] = 0.0f;
+
+ // Remove y scale.
+ if (decomposedScale.y != 0.0f) {
+ for (int i = 4; i < 7; i++) {
+ destMatrix.data[i] = data[i] / decomposedScale.y;
+ }
+ }
+
+ destMatrix.data[7] = 0.0f;
+
+ // Remove z scale.
+ if (decomposedScale.z != 0.0f) {
+ for (int i = 8; i < 11; i++) {
+ destMatrix.data[i] = data[i] / decomposedScale.z;
+ }
+ }
+
+ destMatrix.data[11] = 0.0f;
+ destMatrix.data[12] = 0.0f;
+ destMatrix.data[13] = 0.0f;
+ destMatrix.data[14] = 0.0f;
+ destMatrix.data[15] = 1.0f;
+ }
+
+ public void extractQuaternion(Quaternion destQuaternion) {
+ float trace = data[0] + data[5] + data[10];
+
+ if (trace > 0) {
+ float s = (float) Math.sqrt(trace + 1.0) * 2.0f;
+ destQuaternion.w = 0.25f * s;
+ destQuaternion.x = (data[6] - data[9]) / s;
+ destQuaternion.y = (data[8] - data[2]) / s;
+ destQuaternion.z = (data[1] - data[4]) / s;
+ } else if ((data[0] > data[5]) && (data[0] > data[10])) {
+ float s = (float) Math.sqrt(1.0f + data[0] - data[5] - data[10]) * 2.0f;
+ destQuaternion.w = (data[6] - data[9]) / s;
+ destQuaternion.x = 0.25f * s;
+ destQuaternion.y = (data[4] + data[1]) / s;
+ destQuaternion.z = (data[8] + data[2]) / s;
+ } else if (data[5] > data[10]) {
+ float s = (float) Math.sqrt(1.0f + data[5] - data[0] - data[10]) * 2.0f;
+ destQuaternion.w = (data[8] - data[2]) / s;
+ destQuaternion.x = (data[4] + data[1]) / s;
+ destQuaternion.y = 0.25f * s;
+ destQuaternion.z = (data[9] + data[6]) / s;
+ } else {
+ float s = (float) Math.sqrt(1.0f + data[10] - data[0] - data[5]) * 2.0f;
+ destQuaternion.w = (data[1] - data[4]) / s;
+ destQuaternion.x = (data[8] + data[2]) / s;
+ destQuaternion.y = (data[9] + data[6]) / s;
+ destQuaternion.z = 0.25f * s;
+ }
+ destQuaternion.normalize();
+ }
+
+ public void makeTranslation(Vector3 translation) {
+ Preconditions.checkNotNull(translation, "Parameter \"translation\" was null.");
+
+ set(IDENTITY_DATA);
+
+ setTranslation(translation);
+ }
+
+ public void setTranslation(Vector3 translation) {
+ data[12] = translation.x;
+ data[13] = translation.y;
+ data[14] = translation.z;
+ }
+
+ public void makeRotation(Quaternion rotation) {
+ Preconditions.checkNotNull(rotation, "Parameter \"rotation\" was null.");
+
+ set(IDENTITY_DATA);
+
+ rotation.normalize();
+
+ float xx = rotation.x * rotation.x;
+ float xy = rotation.x * rotation.y;
+ float xz = rotation.x * rotation.z;
+ float xw = rotation.x * rotation.w;
+
+ float yy = rotation.y * rotation.y;
+ float yz = rotation.y * rotation.z;
+ float yw = rotation.y * rotation.w;
+
+ float zz = rotation.z * rotation.z;
+ float zw = rotation.z * rotation.w;
+
+ data[0] = 1.0f - 2.0f * (yy + zz);
+ data[4] = 2.0f * (xy - zw);
+ data[8] = 2.0f * (xz + yw);
+
+ data[1] = 2.0f * (xy + zw);
+ data[5] = 1.0f - 2.0f * (xx + zz);
+ data[9] = 2.0f * (yz - xw);
+
+ data[2] = 2.0f * (xz - yw);
+ data[6] = 2.0f * (yz + xw);
+ data[10] = 1.0f - 2.0f * (xx + yy);
+ }
+
+ public void makeScale(float scale) {
+ Preconditions.checkNotNull(scale, "Parameter \"scale\" was null.");
+
+ set(IDENTITY_DATA);
+
+ data[0] = scale;
+ data[5] = scale;
+ data[10] = scale;
+ }
+
+ public void makeScale(Vector3 scale) {
+ Preconditions.checkNotNull(scale, "Parameter \"scale\" was null.");
+
+ set(IDENTITY_DATA);
+
+ data[0] = scale.x;
+ data[5] = scale.y;
+ data[10] = scale.z;
+ }
+
+ public void makeTrs(Vector3 translation, Quaternion rotation, Vector3 scale) {
+ float mdsqx = 1 - 2 * rotation.x * rotation.x;
+ float sqy = rotation.y * rotation.y;
+ float dsqz = 2 * rotation.z * rotation.z;
+ float dqxz = 2 * rotation.x * rotation.z;
+ float dqyw = 2 * rotation.y * rotation.w;
+ float dqxy = 2 * rotation.x * rotation.y;
+ float dqzw = 2 * rotation.z * rotation.w;
+ float dqxw = 2 * rotation.x * rotation.w;
+ float dqyz = 2 * rotation.y * rotation.z;
+
+ data[0] = (1 - 2 * sqy - dsqz) * scale.x;
+ data[4] = (dqxy - dqzw) * scale.y;
+ data[8] = (dqxz + dqyw) * scale.z;
+
+ data[1] = (dqxy + dqzw) * scale.x;
+ data[5] = (mdsqx - dsqz) * scale.y;
+ data[9] = (dqyz - dqxw) * scale.z;
+
+ data[2] = (dqxz - dqyw) * scale.x;
+ data[6] = (dqyz + dqxw) * scale.y;
+ data[10] = (mdsqx - 2 * sqy) * scale.z;
+
+ data[12] = translation.x;
+ data[13] = translation.y;
+ data[14] = translation.z;
+ data[15] = 1.0f;
+ }
+
+ public static void multiply(Matrix lhs, Matrix rhs, Matrix dest) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+
+ float m00 = 0f;
+ float m01 = 0f;
+ float m02 = 0f;
+ float m03 = 0f;
+ float m10 = 0f;
+ float m11 = 0f;
+ float m12 = 0f;
+ float m13 = 0f;
+ float m20 = 0f;
+ float m21 = 0f;
+ float m22 = 0f;
+ float m23 = 0f;
+ float m30 = 0f;
+ float m31 = 0f;
+ float m32 = 0f;
+ float m33 = 0f;
+
+ for (int i = 0; i < 4; i++) {
+ float lhs0 = lhs.data[0 + (i * 4)];
+ float lhs1 = lhs.data[1 + (i * 4)];
+ float lhs2 = lhs.data[2 + (i * 4)];
+ float lhs3 = lhs.data[3 + (i * 4)];
+ float rhs0 = rhs.data[(0 * 4) + i];
+ float rhs1 = rhs.data[(1 * 4) + i];
+ float rhs2 = rhs.data[(2 * 4) + i];
+ float rhs3 = rhs.data[(3 * 4) + i];
+
+ m00 += lhs0 * rhs0;
+ m01 += lhs1 * rhs0;
+ m02 += lhs2 * rhs0;
+ m03 += lhs3 * rhs0;
+
+ m10 += lhs0 * rhs1;
+ m11 += lhs1 * rhs1;
+ m12 += lhs2 * rhs1;
+ m13 += lhs3 * rhs1;
+
+ m20 += lhs0 * rhs2;
+ m21 += lhs1 * rhs2;
+ m22 += lhs2 * rhs2;
+ m23 += lhs3 * rhs2;
+
+ m30 += lhs0 * rhs3;
+ m31 += lhs1 * rhs3;
+ m32 += lhs2 * rhs3;
+ m33 += lhs3 * rhs3;
+ }
+
+ dest.data[0] = m00;
+ dest.data[1] = m01;
+ dest.data[2] = m02;
+ dest.data[3] = m03;
+ dest.data[4] = m10;
+ dest.data[5] = m11;
+ dest.data[6] = m12;
+ dest.data[7] = m13;
+ dest.data[8] = m20;
+ dest.data[9] = m21;
+ dest.data[10] = m22;
+ dest.data[11] = m23;
+ dest.data[12] = m30;
+ dest.data[13] = m31;
+ dest.data[14] = m32;
+ dest.data[15] = m33;
+ }
+
+ public Vector3 transformPoint(Vector3 vector) {
+ Preconditions.checkNotNull(vector, "Parameter \"vector\" was null.");
+
+ Vector3 result = new Vector3();
+ float vx = vector.x;
+ float vy = vector.y;
+ float vz = vector.z;
+ result.x = data[0] * vx;
+ result.x += data[4] * vy;
+ result.x += data[8] * vz;
+ result.x += data[12]; // *1
+
+ result.y = data[1] * vx;
+ result.y += data[5] * vy;
+ result.y += data[9] * vz;
+ result.y += data[13]; // *1
+
+ result.z = data[2] * vx;
+ result.z += data[6] * vy;
+ result.z += data[10] * vz;
+ result.z += data[14]; // *1
+ return result;
+ }
+
+ /**
+ * Transforms a direction by ignoring any translation.
+ *
+ * If the matrix is uniformly (positively) scaled, then the resulting direction will be correct
+ * but scaled by the same factor. If a unit direction is required then the result should be
+ * normalized.
+ *
+ *
If the scale is non-uniform or negative then the result vector will be distorted. In this
+ * case the matrix used should be the inverse transpose of the incoming matrix.
+ */
+ public Vector3 transformDirection(Vector3 vector) {
+ Preconditions.checkNotNull(vector, "Parameter \"vector\" was null.");
+
+ Vector3 result = new Vector3();
+ float vx = vector.x;
+ float vy = vector.y;
+ float vz = vector.z;
+ result.x = data[0] * vx;
+ result.x += data[4] * vy;
+ result.x += data[8] * vz;
+
+ result.y = data[1] * vx;
+ result.y += data[5] * vy;
+ result.y += data[9] * vz;
+
+ result.z = data[2] * vx;
+ result.z += data[6] * vy;
+ result.z += data[10] * vz;
+ return result;
+ }
+
+ public static boolean invert(Matrix matrix, Matrix dest) {
+ Preconditions.checkNotNull(matrix, "Parameter \"matrix\" was null.");
+ Preconditions.checkNotNull(dest, "Parameter \"dest\" was null.");
+
+ float m0 = matrix.data[0];
+ float m1 = matrix.data[1];
+ float m2 = matrix.data[2];
+ float m3 = matrix.data[3];
+ float m4 = matrix.data[4];
+ float m5 = matrix.data[5];
+ float m6 = matrix.data[6];
+ float m7 = matrix.data[7];
+ float m8 = matrix.data[8];
+ float m9 = matrix.data[9];
+ float m10 = matrix.data[10];
+ float m11 = matrix.data[11];
+ float m12 = matrix.data[12];
+ float m13 = matrix.data[13];
+ float m14 = matrix.data[14];
+ float m15 = matrix.data[15];
+
+ dest.data[0] =
+ m5 * m10 * m15
+ - m5 * m11 * m14
+ - m9 * m6 * m15
+ + m9 * m7 * m14
+ + m13 * m6 * m11
+ - m13 * m7 * m10;
+
+ dest.data[4] =
+ -m4 * m10 * m15
+ + m4 * m11 * m14
+ + m8 * m6 * m15
+ - m8 * m7 * m14
+ - m12 * m6 * m11
+ + m12 * m7 * m10;
+
+ dest.data[8] =
+ m4 * m9 * m15
+ - m4 * m11 * m13
+ - m8 * m5 * m15
+ + m8 * m7 * m13
+ + m12 * m5 * m11
+ - m12 * m7 * m9;
+
+ dest.data[12] =
+ -m4 * m9 * m14
+ + m4 * m10 * m13
+ + m8 * m5 * m14
+ - m8 * m6 * m13
+ - m12 * m5 * m10
+ + m12 * m6 * m9;
+
+ dest.data[1] =
+ -m1 * m10 * m15
+ + m1 * m11 * m14
+ + m9 * m2 * m15
+ - m9 * m3 * m14
+ - m13 * m2 * m11
+ + m13 * m3 * m10;
+
+ dest.data[5] =
+ m0 * m10 * m15
+ - m0 * m11 * m14
+ - m8 * m2 * m15
+ + m8 * m3 * m14
+ + m12 * m2 * m11
+ - m12 * m3 * m10;
+
+ dest.data[9] =
+ -m0 * m9 * m15
+ + m0 * m11 * m13
+ + m8 * m1 * m15
+ - m8 * m3 * m13
+ - m12 * m1 * m11
+ + m12 * m3 * m9;
+
+ dest.data[13] =
+ m0 * m9 * m14
+ - m0 * m10 * m13
+ - m8 * m1 * m14
+ + m8 * m2 * m13
+ + m12 * m1 * m10
+ - m12 * m2 * m9;
+
+ dest.data[2] =
+ m1 * m6 * m15
+ - m1 * m7 * m14
+ - m5 * m2 * m15
+ + m5 * m3 * m14
+ + m13 * m2 * m7
+ - m13 * m3 * m6;
+
+ dest.data[6] =
+ -m0 * m6 * m15
+ + m0 * m7 * m14
+ + m4 * m2 * m15
+ - m4 * m3 * m14
+ - m12 * m2 * m7
+ + m12 * m3 * m6;
+
+ dest.data[10] =
+ m0 * m5 * m15
+ - m0 * m7 * m13
+ - m4 * m1 * m15
+ + m4 * m3 * m13
+ + m12 * m1 * m7
+ - m12 * m3 * m5;
+
+ dest.data[14] =
+ -m0 * m5 * m14
+ + m0 * m6 * m13
+ + m4 * m1 * m14
+ - m4 * m2 * m13
+ - m12 * m1 * m6
+ + m12 * m2 * m5;
+
+ dest.data[3] =
+ -m1 * m6 * m11
+ + m1 * m7 * m10
+ + m5 * m2 * m11
+ - m5 * m3 * m10
+ - m9 * m2 * m7
+ + m9 * m3 * m6;
+
+ dest.data[7] =
+ m0 * m6 * m11 - m0 * m7 * m10 - m4 * m2 * m11 + m4 * m3 * m10 + m8 * m2 * m7 - m8 * m3 * m6;
+
+ dest.data[11] =
+ -m0 * m5 * m11 + m0 * m7 * m9 + m4 * m1 * m11 - m4 * m3 * m9 - m8 * m1 * m7 + m8 * m3 * m5;
+
+ dest.data[15] =
+ m0 * m5 * m10 - m0 * m6 * m9 - m4 * m1 * m10 + m4 * m2 * m9 + m8 * m1 * m6 - m8 * m2 * m5;
+
+ float det = m0 * dest.data[0] + m1 * dest.data[4] + m2 * dest.data[8] + m3 * dest.data[12];
+
+ if (det == 0) {
+ return false;
+ }
+
+ det = 1.0f / det;
+
+ for (int i = 0; i < 16; i++) {
+ dest.data[i] *= det;
+ }
+
+ return true;
+ }
+
+ /** Compares Matrix values */
+ public static boolean equals(Matrix lhs, Matrix rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+
+ boolean result = true;
+ for (int i = 0; i < 16; i++) {
+ result &= MathHelper.almostEqualRelativeAndAbs(lhs.data[i], rhs.data[i]);
+ }
+ return result;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Quaternion.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Quaternion.java
new file mode 100644
index 0000000..575684c
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Quaternion.java
@@ -0,0 +1,494 @@
+package com.google.ar.sceneform.math;
+
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * A Sceneform quaternion class for floats.
+ *
+ *
Quaternion operations are Hamiltonian using the right-hand-rule convention.
+ */
+// TODO: Evaluate combining with java/com/google/ar/core/Quaternion.java
+public class Quaternion {
+ private static final float SLERP_THRESHOLD = 0.9995f;
+ public float x;
+ public float y;
+ public float z;
+ public float w;
+
+ /** Construct Quaternion and set to Identity */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Quaternion() {
+ x = 0;
+ y = 0;
+ z = 0;
+ w = 1;
+ }
+
+ /**
+ * Construct Quaternion and set each value. The Quaternion will be normalized during construction
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Quaternion(float x, float y, float z, float w) {
+ set(x, y, z, w);
+ }
+
+ /** Construct Quaternion using values from another Quaternion */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Quaternion(Quaternion q) {
+ Preconditions.checkNotNull(q, "Parameter \"q\" was null.");
+ set(q);
+ }
+
+ /**
+ * Construct Quaternion using an axis/angle to define the rotation
+ *
+ * @param axis Sets rotation direction
+ * @param angle Angle size in degrees
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Quaternion(Vector3 axis, float angle) {
+ Preconditions.checkNotNull(axis, "Parameter \"axis\" was null.");
+ set(Quaternion.axisAngle(axis, angle));
+ }
+
+ /**
+ * Construct Quaternion based on eulerAngles.
+ *
+ * @see #eulerAngles(Vector3 eulerAngles)
+ * @param eulerAngles - the angle in degrees for each axis.
+ */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Quaternion(Vector3 eulerAngles) {
+ Preconditions.checkNotNull(eulerAngles, "Parameter \"eulerAngles\" was null.");
+ set(Quaternion.eulerAngles(eulerAngles));
+ }
+
+ /** Copy values from another Quaternion into this one */
+ public void set(Quaternion q) {
+ Preconditions.checkNotNull(q, "Parameter \"q\" was null.");
+ x = q.x;
+ y = q.y;
+ z = q.z;
+ w = q.w;
+ normalize();
+ }
+
+ /** Update this Quaternion using an axis/angle to define the rotation */
+ public void set(Vector3 axis, float angle) {
+ Preconditions.checkNotNull(axis, "Parameter \"axis\" was null.");
+ set(Quaternion.axisAngle(axis, angle));
+ }
+
+ /** Set each value and normalize the Quaternion */
+ public void set(float qx, float qy, float qz, float qw) {
+ x = qx;
+ y = qy;
+ z = qz;
+ w = qw;
+ normalize();
+ }
+
+ /** Set the Quaternion to identity */
+ public void setIdentity() {
+ x = 0;
+ y = 0;
+ z = 0;
+ w = 1;
+ }
+
+ /**
+ * Rescales the quaternion to the unit length.
+ *
+ *
If the Quaternion can not be scaled, it is set to identity and false is returned.
+ *
+ * @return true if the Quaternion was non-zero
+ */
+ public boolean normalize() {
+ float normSquared = Quaternion.dot(this, this);
+ if (MathHelper.almostEqualRelativeAndAbs(normSquared, 0.0f)) {
+ setIdentity();
+ return false;
+ } else if (normSquared != 1) {
+ float norm = (float) (1.0 / Math.sqrt(normSquared));
+ x *= norm;
+ y *= norm;
+ z *= norm;
+ w *= norm;
+ } else {
+ // do nothing if normSquared is already the unit length
+ }
+ return true;
+ }
+
+ /**
+ * Get a Quaternion with a matching rotation but scaled to unit length.
+ *
+ * @return the quaternion scaled to the unit length, or zero if that can not be done.
+ */
+ public Quaternion normalized() {
+ Quaternion result = new Quaternion(this);
+ result.normalize();
+ return result;
+ }
+
+ /**
+ * Get a Quaternion with the opposite rotation
+ *
+ * @return the opposite rotation
+ */
+ public Quaternion inverted() {
+ return new Quaternion(-this.x, -this.y, -this.z, this.w);
+ }
+
+ /**
+ * Flips the sign of the Quaternion, but represents the same rotation.
+ *
+ * @return the negated Quaternion
+ */
+ Quaternion negated() {
+ return new Quaternion(-this.x, -this.y, -this.z, -this.w);
+ }
+
+ @Override
+ public String toString() {
+ return "[x=" + x + ", y=" + y + ", z=" + z + ", w=" + w + "]";
+ }
+
+ /**
+ * Rotates a Vector3 by a Quaternion
+ *
+ * @return The rotated vector
+ */
+ public static Vector3 rotateVector(Quaternion q, Vector3 src) {
+ Preconditions.checkNotNull(q, "Parameter \"q\" was null.");
+ Preconditions.checkNotNull(src, "Parameter \"src\" was null.");
+ Vector3 result = new Vector3();
+ float w2 = q.w * q.w;
+ float x2 = q.x * q.x;
+ float y2 = q.y * q.y;
+ float z2 = q.z * q.z;
+ float zw = q.z * q.w;
+ float xy = q.x * q.y;
+ float xz = q.x * q.z;
+ float yw = q.y * q.w;
+ float yz = q.y * q.z;
+ float xw = q.x * q.w;
+ float m00 = w2 + x2 - z2 - y2;
+ float m01 = xy + zw + zw + xy;
+ float m02 = xz - yw + xz - yw;
+ float m10 = -zw + xy - zw + xy;
+ float m11 = y2 - z2 + w2 - x2;
+ float m12 = yz + yz + xw + xw;
+ float m20 = yw + xz + xz + yw;
+ float m21 = yz + yz - xw - xw;
+ float m22 = z2 - y2 - x2 + w2;
+ float sx = src.x;
+ float sy = src.y;
+ float sz = src.z;
+ result.x = m00 * sx + m10 * sy + m20 * sz;
+ result.y = m01 * sx + m11 * sy + m21 * sz;
+ result.z = m02 * sx + m12 * sy + m22 * sz;
+ return result;
+ }
+
+ public static Vector3 inverseRotateVector(Quaternion q, Vector3 src) {
+ Preconditions.checkNotNull(q, "Parameter \"q\" was null.");
+ Preconditions.checkNotNull(src, "Parameter \"src\" was null.");
+ Vector3 result = new Vector3();
+ float w2 = q.w * q.w;
+ float x2 = -q.x * -q.x;
+ float y2 = -q.y * -q.y;
+ float z2 = -q.z * -q.z;
+ float zw = -q.z * q.w;
+ float xy = -q.x * -q.y;
+ float xz = -q.x * -q.z;
+ float yw = -q.y * q.w;
+ float yz = -q.y * -q.z;
+ float xw = -q.x * q.w;
+ float m00 = w2 + x2 - z2 - y2;
+ float m01 = xy + zw + zw + xy;
+ float m02 = xz - yw + xz - yw;
+ float m10 = -zw + xy - zw + xy;
+ float m11 = y2 - z2 + w2 - x2;
+ float m12 = yz + yz + xw + xw;
+ float m20 = yw + xz + xz + yw;
+ float m21 = yz + yz - xw - xw;
+ float m22 = z2 - y2 - x2 + w2;
+
+ float sx = src.x;
+ float sy = src.y;
+ float sz = src.z;
+ result.x = m00 * sx + m10 * sy + m20 * sz;
+ result.y = m01 * sx + m11 * sy + m21 * sz;
+ result.z = m02 * sx + m12 * sy + m22 * sz;
+ return result;
+ }
+
+ /**
+ * Create a Quaternion by combining two Quaternions multiply(lhs, rhs) is equivalent to performing
+ * the rhs rotation then lhs rotation Ordering is important for this operation.
+ *
+ * @return The combined rotation
+ */
+ public static Quaternion multiply(Quaternion lhs, Quaternion rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ float lx = lhs.x;
+ float ly = lhs.y;
+ float lz = lhs.z;
+ float lw = lhs.w;
+ float rx = rhs.x;
+ float ry = rhs.y;
+ float rz = rhs.z;
+ float rw = rhs.w;
+
+ Quaternion result =
+ new Quaternion(
+ lw * rx + lx * rw + ly * rz - lz * ry,
+ lw * ry - lx * rz + ly * rw + lz * rx,
+ lw * rz + lx * ry - ly * rx + lz * rw,
+ lw * rw - lx * rx - ly * ry - lz * rz);
+ return result;
+ }
+
+ /**
+ * Uniformly scales a Quaternion without normalizing
+ *
+ * @return a Quaternion multiplied by a scalar amount.
+ */
+ Quaternion scaled(float a) {
+ Quaternion result = new Quaternion();
+ result.x = this.x * a;
+ result.y = this.y * a;
+ result.z = this.z * a;
+ result.w = this.w * a;
+
+ return result;
+ }
+
+ /**
+ * Adds two Quaternion's without normalizing
+ *
+ * @return The combined Quaternion
+ */
+ static Quaternion add(Quaternion lhs, Quaternion rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ Quaternion result = new Quaternion();
+ result.x = lhs.x + rhs.x;
+ result.y = lhs.y + rhs.y;
+ result.z = lhs.z + rhs.z;
+ result.w = lhs.w + rhs.w;
+ return result;
+ }
+
+ /** The dot product of two Quaternions. */
+ static float dot(Quaternion lhs, Quaternion rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w;
+ }
+
+ /**
+ * Returns the linear interpolation between two given rotations by a ratio. The ratio is clamped
+ * between a range of 0 and 1.
+ */
+ static Quaternion lerp(Quaternion a, Quaternion b, float ratio) {
+ Preconditions.checkNotNull(a, "Parameter \"a\" was null.");
+ Preconditions.checkNotNull(b, "Parameter \"b\" was null.");
+ return new Quaternion(
+ MathHelper.lerp(a.x, b.x, ratio),
+ MathHelper.lerp(a.y, b.y, ratio),
+ MathHelper.lerp(a.z, b.z, ratio),
+ MathHelper.lerp(a.w, b.w, ratio));
+ }
+
+ /*
+ * Returns the spherical linear interpolation between two given orientations.
+ *
+ * If t is 0 this returns a.
+ * As t approaches 1 {@link #slerp} may approach either b or -b (whichever is closest
+ * to a)
+ * If t is above 1 or below 0 the result will be extrapolated.
+ * @param a the beginning value
+ * @param b the ending value
+ * @param t the ratio between the two floats
+ * @return interpolated value between the two floats
+ */
+ public static Quaternion slerp(final Quaternion start, final Quaternion end, float t) {
+ Preconditions.checkNotNull(start, "Parameter \"start\" was null.");
+ Preconditions.checkNotNull(end, "Parameter \"end\" was null.");
+ Quaternion orientation0 = start.normalized();
+ Quaternion orientation1 = end.normalized();
+
+ // cosTheta0 provides the angle between the rotations at t=0
+ double cosTheta0 = Quaternion.dot(orientation0, orientation1);
+
+ // Flip end rotation to get shortest path if needed
+ if (cosTheta0 < 0.0f) {
+ orientation1 = orientation1.negated();
+ cosTheta0 = -cosTheta0;
+ }
+
+ // Small rotations should just use lerp
+ if (cosTheta0 > SLERP_THRESHOLD) {
+ return lerp(orientation0, orientation1, t);
+ }
+
+ // Cosine function range is -1,1. Clamp larger rotations.
+ cosTheta0 = Math.max(-1, Math.min(1, cosTheta0));
+
+ double theta0 = Math.acos(cosTheta0); // Angle between orientations at t=0
+ double thetaT = theta0 * t; // theta0 scaled to current t
+
+ // s0 = sin(theta0 - thetaT) / sin(theta0)
+ double s0 = (Math.cos(thetaT) - cosTheta0 * Math.sin(thetaT) / Math.sin(theta0));
+ double s1 = (Math.sin(thetaT) / Math.sin(theta0));
+ // result = s0*start + s1*end
+ Quaternion result =
+ Quaternion.add(orientation0.scaled((float) s0), orientation1.scaled((float) s1));
+ return result.normalized();
+ }
+
+ /**
+ * Get a new Quaternion using an axis/angle to define the rotation
+ *
+ * @param axis Sets rotation direction
+ * @param degrees Angle size in degrees
+ */
+ public static Quaternion axisAngle(Vector3 axis, float degrees) {
+ Preconditions.checkNotNull(axis, "Parameter \"axis\" was null.");
+ Quaternion dest = new Quaternion();
+ double angle = Math.toRadians(degrees);
+ double factor = Math.sin(angle / 2.0);
+
+ dest.x = (float) (axis.x * factor);
+ dest.y = (float) (axis.y * factor);
+ dest.z = (float) (axis.z * factor);
+ dest.w = (float) Math.cos(angle / 2.0);
+ dest.normalize();
+ return dest;
+ }
+
+ /**
+ * Get a new Quaternion using eulerAngles to define the rotation.
+ *
+ *
The rotations are applied in Z, Y, X order. This is consistent with other graphics engines.
+ * One thing to note is the coordinate systems are different between Sceneform and Unity, so the
+ * same angles used here will have cause a different orientation than Unity. Carefully check your
+ * parameter values to get the same effect as in other engines.
+ *
+ * @param eulerAngles - the angles in degrees.
+ */
+ public static Quaternion eulerAngles(Vector3 eulerAngles) {
+ Preconditions.checkNotNull(eulerAngles, "Parameter \"eulerAngles\" was null.");
+ Quaternion qX = new Quaternion(Vector3.right(), eulerAngles.x);
+ Quaternion qY = new Quaternion(Vector3.up(), eulerAngles.y);
+ Quaternion qZ = new Quaternion(Vector3.back(), eulerAngles.z);
+ return Quaternion.multiply(Quaternion.multiply(qY, qX), qZ);
+ }
+
+ /** Get a new Quaternion representing the rotation from one vector to another. */
+ public static Quaternion rotationBetweenVectors(Vector3 start, Vector3 end) {
+ Preconditions.checkNotNull(start, "Parameter \"start\" was null.");
+ Preconditions.checkNotNull(end, "Parameter \"end\" was null.");
+
+ start = start.normalized();
+ end = end.normalized();
+
+ float cosTheta = Vector3.dot(start, end);
+ Vector3 rotationAxis;
+
+ if (cosTheta < -1.0f + 0.001f) {
+ // special case when vectors in opposite directions:
+ // there is no "ideal" rotation axis
+ // So guess one; any will do as long as it's perpendicular to start
+ rotationAxis = Vector3.cross(Vector3.back(), start);
+ if (rotationAxis.lengthSquared() < 0.01f) { // bad luck, they were parallel, try again!
+ rotationAxis = Vector3.cross(Vector3.right(), start);
+ }
+
+ rotationAxis = rotationAxis.normalized();
+ return axisAngle(rotationAxis, 180.0f);
+ }
+
+ rotationAxis = Vector3.cross(start, end);
+
+ float squareLength = (float) Math.sqrt((1.0 + cosTheta) * 2.0);
+ float inverseSquareLength = 1.0f / squareLength;
+
+ return new Quaternion(
+ rotationAxis.x * inverseSquareLength,
+ rotationAxis.y * inverseSquareLength,
+ rotationAxis.z * inverseSquareLength,
+ squareLength * 0.5f);
+ }
+
+ /**
+ * Get a new Quaternion representing a rotation towards a specified forward direction. If
+ * upInWorld is orthogonal to forwardInWorld, then the Y axis is aligned with desiredUpInWorld.
+ */
+ public static Quaternion lookRotation(Vector3 forwardInWorld, Vector3 desiredUpInWorld) {
+ Preconditions.checkNotNull(forwardInWorld, "Parameter \"forwardInWorld\" was null.");
+ Preconditions.checkNotNull(desiredUpInWorld, "Parameter \"desiredUpInWorld\" was null.");
+
+ // Find the rotation between the world forward and the forward to look at.
+ Quaternion rotateForwardToDesiredForward =
+ rotationBetweenVectors(Vector3.forward(), forwardInWorld);
+
+ // Recompute upwards so that it's perpendicular to the direction
+ Vector3 rightInWorld = Vector3.cross(forwardInWorld, desiredUpInWorld);
+ desiredUpInWorld = Vector3.cross(rightInWorld, forwardInWorld);
+
+ // Find the rotation between the "up" of the rotated object, and the desired up
+ Vector3 newUp = Quaternion.rotateVector(rotateForwardToDesiredForward, Vector3.up());
+ Quaternion rotateNewUpToUpwards = rotationBetweenVectors(newUp, desiredUpInWorld);
+
+ return Quaternion.multiply(rotateNewUpToUpwards, rotateForwardToDesiredForward);
+ }
+
+ /**
+ * Compare two Quaternions
+ *
+ *
Tests for equality by calculating the dot product of lhs and rhs. lhs and -lhs will not be
+ * equal according to this function.
+ */
+ public static boolean equals(Quaternion lhs, Quaternion rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ float dot = Quaternion.dot(lhs, rhs);
+ return MathHelper.almostEqualRelativeAndAbs(dot, 1.0f);
+ }
+
+ /**
+ * Returns true if the other object is a Quaternion and the dot product is 1.0 +/- a tolerance.
+ */
+ @Override
+ @SuppressWarnings("override.param.invalid")
+ public boolean equals(Object other) {
+ if (!(other instanceof Quaternion)) {
+ return false;
+ }
+ if (this == other) {
+ return true;
+ }
+ return Quaternion.equals(this, (Quaternion) other);
+ }
+
+ /** @hide */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Float.floatToIntBits(w);
+ result = prime * result + Float.floatToIntBits(x);
+ result = prime * result + Float.floatToIntBits(y);
+ result = prime * result + Float.floatToIntBits(z);
+ return result;
+ }
+
+ /** Get a Quaternion set to identity */
+ public static Quaternion identity() {
+ return new Quaternion();
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/QuaternionEvaluator.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/QuaternionEvaluator.java
new file mode 100644
index 0000000..c9fbc2d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/QuaternionEvaluator.java
@@ -0,0 +1,11 @@
+package com.google.ar.sceneform.math;
+
+import android.animation.TypeEvaluator;
+
+/** TypeEvaluator for Quaternions. Used to animate rotations. */
+public class QuaternionEvaluator implements TypeEvaluator {
+ @Override
+ public Quaternion evaluate(float fraction, Quaternion startValue, Quaternion endValue) {
+ return Quaternion.slerp(startValue, endValue, fraction);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Vector3.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Vector3.java
new file mode 100644
index 0000000..92092ee
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Vector3.java
@@ -0,0 +1,339 @@
+package com.google.ar.sceneform.math;
+
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/** A Vector with 3 floats. */
+// TODO: Evaluate consolidating internal math. Additional bugs: b/69935335
+public class Vector3 {
+ public float x;
+ public float y;
+ public float z;
+
+ /** Construct a Vector3 and assign zero to all values */
+ public Vector3() {
+ x = 0;
+ y = 0;
+ z = 0;
+ }
+
+ /** Construct a Vector3 and assign each value */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Vector3(float x, float y, float z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ /** Construct a Vector3 and copy the values */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public Vector3(Vector3 v) {
+ Preconditions.checkNotNull(v, "Parameter \"v\" was null.");
+ set(v);
+ }
+
+ /** Copy the values from another Vector3 to this Vector3 */
+ public void set(Vector3 v) {
+ Preconditions.checkNotNull(v, "Parameter \"v\" was null.");
+ x = v.x;
+ y = v.y;
+ z = v.z;
+ }
+
+ /** Set each value */
+ public void set(float vx, float vy, float vz) {
+ x = vx;
+ y = vy;
+ z = vz;
+ }
+
+ /** Set each value to zero */
+ void setZero() {
+ set(0, 0, 0);
+ }
+
+ /** Set each value to one */
+ void setOne() {
+ set(1, 1, 1);
+ }
+
+ /** Forward into the screen is the negative Z direction */
+ void setForward() {
+ set(0, 0, -1);
+ }
+
+ /** Back out of the screen is the positive Z direction */
+ void setBack() {
+ set(0, 0, 1);
+ }
+
+ /** Up is the positive Y direction */
+ void setUp() {
+ set(0, 1, 0);
+ }
+
+ /** Down is the negative Y direction */
+ void setDown() {
+ set(0, -1, 0);
+ }
+
+ /** Right is the positive X direction */
+ void setRight() {
+ set(1, 0, 0);
+ }
+
+ /** Left is the negative X direction */
+ void setLeft() {
+ set(-1, 0, 0);
+ }
+
+ public float lengthSquared() {
+ return x * x + y * y + z * z;
+ }
+
+ public float length() {
+ return (float) Math.sqrt(lengthSquared());
+ }
+
+ @Override
+ public String toString() {
+ return "[x=" + x + ", y=" + y + ", z=" + z + "]";
+ }
+
+ /** Scales the Vector3 to the unit length */
+ public Vector3 normalized() {
+ Vector3 result = new Vector3(this);
+ float normSquared = Vector3.dot(this, this);
+
+ if (MathHelper.almostEqualRelativeAndAbs(normSquared, 0.0f)) {
+ result.setZero();
+ } else if (normSquared != 1) {
+ float norm = (float) (1.0 / Math.sqrt(normSquared));
+ result.set(this.scaled(norm));
+ }
+ return result;
+ }
+
+ /**
+ * Uniformly scales a Vector3
+ *
+ * @return a Vector3 multiplied by a scalar amount
+ */
+ public Vector3 scaled(float a) {
+ return new Vector3(x * a, y * a, z * a);
+ }
+
+ /**
+ * Negates a Vector3
+ *
+ * @return A Vector3 with opposite direction
+ */
+ public Vector3 negated() {
+ return new Vector3(-x, -y, -z);
+ }
+
+ /**
+ * Adds two Vector3's
+ *
+ * @return The combined Vector3
+ */
+ public static Vector3 add(Vector3 lhs, Vector3 rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ return new Vector3(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z);
+ }
+
+ /**
+ * Subtract two Vector3
+ *
+ * @return The combined Vector3
+ */
+ public static Vector3 subtract(Vector3 lhs, Vector3 rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ return new Vector3(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z);
+ }
+
+ /**
+ * Get dot product of two Vector3's
+ *
+ * @return The scalar product of the Vector3's
+ */
+ public static float dot(Vector3 lhs, Vector3 rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
+ }
+
+ /**
+ * Get cross product of two Vector3's
+ *
+ * @return A Vector3 perpendicular to Vector3's
+ */
+ public static Vector3 cross(Vector3 lhs, Vector3 rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ float lhsX = lhs.x;
+ float lhsY = lhs.y;
+ float lhsZ = lhs.z;
+ float rhsX = rhs.x;
+ float rhsY = rhs.y;
+ float rhsZ = rhs.z;
+ return new Vector3(
+ lhsY * rhsZ - lhsZ * rhsY, lhsZ * rhsX - lhsX * rhsZ, lhsX * rhsY - lhsY * rhsX);
+ }
+
+ /** Get a Vector3 with each value set to the element wise minimum of two Vector3's values */
+ public static Vector3 min(Vector3 lhs, Vector3 rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ return new Vector3(Math.min(lhs.x, rhs.x), Math.min(lhs.y, rhs.y), Math.min(lhs.z, rhs.z));
+ }
+
+ /** Get a Vector3 with each value set to the element wise maximum of two Vector3's values */
+ public static Vector3 max(Vector3 lhs, Vector3 rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ return new Vector3(Math.max(lhs.x, rhs.x), Math.max(lhs.y, rhs.y), Math.max(lhs.z, rhs.z));
+ }
+
+ /** Get the maximum value in a single Vector3 */
+ static float componentMax(Vector3 a) {
+ Preconditions.checkNotNull(a, "Parameter \"a\" was null.");
+ return Math.max(Math.max(a.x, a.y), a.z);
+ }
+
+ /** Get the minimum value in a single Vector3 */
+ static float componentMin(Vector3 a) {
+ Preconditions.checkNotNull(a, "Parameter \"a\" was null.");
+ return Math.min(Math.min(a.x, a.y), a.z);
+ }
+
+ /**
+ * Linearly interpolates between a and b.
+ *
+ * @param a the beginning value
+ * @param b the ending value
+ * @param t ratio between the two floats.
+ * @return interpolated value between the two floats
+ */
+ public static Vector3 lerp(Vector3 a, Vector3 b, float t) {
+ Preconditions.checkNotNull(a, "Parameter \"a\" was null.");
+ Preconditions.checkNotNull(b, "Parameter \"b\" was null.");
+ return new Vector3(
+ MathHelper.lerp(a.x, b.x, t), MathHelper.lerp(a.y, b.y, t), MathHelper.lerp(a.z, b.z, t));
+ }
+
+ /**
+ * Returns the shortest angle in degrees between two vectors. The result is never greater than 180
+ * degrees.
+ */
+ public static float angleBetweenVectors(Vector3 a, Vector3 b) {
+ float lengthA = a.length();
+ float lengthB = b.length();
+ float combinedLength = lengthA * lengthB;
+
+ if (MathHelper.almostEqualRelativeAndAbs(combinedLength, 0.0f)) {
+ return 0.0f;
+ }
+
+ float dot = Vector3.dot(a, b);
+ float cos = dot / combinedLength;
+
+ // Clamp due to floating point precision that could cause dot to be > combinedLength.
+ // Which would cause acos to return NaN.
+ cos = MathHelper.clamp(cos, -1.0f, 1.0f);
+ float angleRadians = (float) Math.acos(cos);
+ return (float) Math.toDegrees(angleRadians);
+ }
+
+ /** Compares two Vector3's are equal if each component is equal within a tolerance. */
+ public static boolean equals(Vector3 lhs, Vector3 rhs) {
+ Preconditions.checkNotNull(lhs, "Parameter \"lhs\" was null.");
+ Preconditions.checkNotNull(rhs, "Parameter \"rhs\" was null.");
+ boolean result = true;
+ result &= MathHelper.almostEqualRelativeAndAbs(lhs.x, rhs.x);
+ result &= MathHelper.almostEqualRelativeAndAbs(lhs.y, rhs.y);
+ result &= MathHelper.almostEqualRelativeAndAbs(lhs.z, rhs.z);
+ return result;
+ }
+
+ /**
+ * Returns true if the other object is a Vector3 and each component is equal within a tolerance.
+ */
+ @Override
+ @SuppressWarnings("override.param.invalid")
+ public boolean equals(Object other) {
+ if (!(other instanceof Vector3)) {
+ return false;
+ }
+ if (this == other) {
+ return true;
+ }
+ return Vector3.equals(this, (Vector3) other);
+ }
+
+ /** @hide */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Float.floatToIntBits(x);
+ result = prime * result + Float.floatToIntBits(y);
+ result = prime * result + Float.floatToIntBits(z);
+ return result;
+ }
+
+ /** Gets a Vector3 with all values set to zero */
+ public static Vector3 zero() {
+ return new Vector3();
+ }
+
+ /** Gets a Vector3 with all values set to one */
+ public static Vector3 one() {
+ Vector3 result = new Vector3();
+ result.setOne();
+ return result;
+ }
+
+ /** Gets a Vector3 set to (0, 0, -1) */
+ public static Vector3 forward() {
+ Vector3 result = new Vector3();
+ result.setForward();
+ return result;
+ }
+
+ /** Gets a Vector3 set to (0, 0, 1) */
+ public static Vector3 back() {
+ Vector3 result = new Vector3();
+ result.setBack();
+ return result;
+ }
+
+ /** Gets a Vector3 set to (0, 1, 0) */
+ public static Vector3 up() {
+ Vector3 result = new Vector3();
+ result.setUp();
+ return result;
+ }
+
+ /** Gets a Vector3 set to (0, -1, 0) */
+ public static Vector3 down() {
+ Vector3 result = new Vector3();
+ result.setDown();
+ return result;
+ }
+
+ /** Gets a Vector3 set to (1, 0, 0) */
+ public static Vector3 right() {
+ Vector3 result = new Vector3();
+ result.setRight();
+ return result;
+ }
+
+ /** Gets a Vector3 set to (-1, 0, 0) */
+ public static Vector3 left() {
+ Vector3 result = new Vector3();
+ result.setLeft();
+ return result;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Vector3Evaluator.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Vector3Evaluator.java
new file mode 100644
index 0000000..0f5f521
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/math/Vector3Evaluator.java
@@ -0,0 +1,11 @@
+package com.google.ar.sceneform.math;
+
+import android.animation.TypeEvaluator;
+
+/** TypeEvaluator for Vector3. Used to animate positions and other vectors. */
+public class Vector3Evaluator implements TypeEvaluator {
+ @Override
+ public Vector3 evaluate(float fraction, Vector3 startValue, Vector3 endValue) {
+ return Vector3.lerp(startValue, endValue, fraction);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CameraProvider.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CameraProvider.java
new file mode 100644
index 0000000..b300aaa
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CameraProvider.java
@@ -0,0 +1,23 @@
+package com.google.ar.sceneform.rendering;
+
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.math.Matrix;
+
+/**
+ * Required interface for a virtual camera.
+ *
+ * @hide
+ */
+public interface CameraProvider extends TransformProvider {
+ boolean isActive();
+
+ float getNearClipPlane();
+
+ float getFarClipPlane();
+
+ Matrix getViewMatrix();
+
+ Matrix getProjectionMatrix();
+
+ void updateTrackedPose(com.google.ar.core.Camera camera);
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CameraStream.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CameraStream.java
new file mode 100644
index 0000000..0da944f
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CameraStream.java
@@ -0,0 +1,309 @@
+package com.google.ar.sceneform.rendering;
+
+import android.util.Log;
+import androidx.annotation.Nullable;
+import com.google.android.filament.EntityManager;
+import com.google.android.filament.IndexBuffer;
+import com.google.android.filament.IndexBuffer.Builder.IndexType;
+import com.google.android.filament.RenderableManager;
+import com.google.android.filament.Scene;
+import com.google.android.filament.VertexBuffer;
+import com.google.android.filament.VertexBuffer.Builder;
+import com.google.android.filament.VertexBuffer.VertexAttribute;
+import com.google.ar.core.Camera;
+import com.google.ar.core.CameraIntrinsics;
+import com.google.ar.core.Frame;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Displays the Camera stream using Filament.
+ *
+ * @hide Note: The class is hidden because it should only be used by the Filament Renderer and does
+ * not expose a user facing API.
+ */
+@SuppressWarnings("AndroidApiChecker") // CompletableFuture
+public class CameraStream {
+ public static final String MATERIAL_CAMERA_TEXTURE = "cameraTexture";
+
+ private static final String TAG = CameraStream.class.getSimpleName();
+ private static final short[] CAMERA_INDICES = new short[] {0, 1, 2};
+ private static final int VERTEX_COUNT = 3;
+ private static final int POSITION_BUFFER_INDEX = 0;
+ private static final int UV_BUFFER_INDEX = 1;
+ private static final int FLOAT_SIZE_IN_BYTES = Float.SIZE / 8;
+
+ private static final float[] CAMERA_VERTICES =
+ new float[] {-1.0f, 1.0f, 1.0f, -1.0f, -3.0f, 1.0f, 3.0f, 1.0f, 1.0f};
+ private static final float[] CAMERA_UVS = new float[] {0.0f, 0.0f, 0.0f, 2.0f, 2.0f, 0.0f};
+
+ private static final int UNINITIALIZED_FILAMENT_RENDERABLE = -1;
+
+ private final Scene scene;
+ private final int cameraTextureId;
+
+ private int cameraStreamRenderable = UNINITIALIZED_FILAMENT_RENDERABLE;
+
+ private final IndexBuffer cameraIndexBuffer;
+ private final VertexBuffer cameraVertexBuffer;
+ private final FloatBuffer cameraUvCoords;
+ private final FloatBuffer transformedCameraUvCoords;
+
+ @Nullable private ExternalTexture cameraTexture;
+
+ @Nullable private Material defaultCameraMaterial = null;
+ @Nullable private Material cameraMaterial = null;
+
+ private int renderablePriority = Renderable.RENDER_PRIORITY_LAST;
+
+ private boolean isTextureInitialized = false;
+
+ @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored", "initialization"})
+ public CameraStream(int cameraTextureId, Renderer renderer) {
+ scene = renderer.getFilamentScene();
+ this.cameraTextureId = cameraTextureId;
+
+ IEngine engine = EngineInstance.getEngine();
+
+ // create screen quad geometry to camera stream to
+ ShortBuffer indexBufferData = ShortBuffer.allocate(CAMERA_INDICES.length);
+ indexBufferData.put(CAMERA_INDICES);
+ final int indexCount = indexBufferData.capacity();
+ cameraIndexBuffer =
+ new IndexBuffer.Builder()
+ .indexCount(indexCount)
+ .bufferType(IndexType.USHORT)
+ .build(engine.getFilamentEngine());
+ indexBufferData.rewind();
+ Preconditions.checkNotNull(cameraIndexBuffer)
+ .setBuffer(engine.getFilamentEngine(), indexBufferData);
+
+ // Note: ARCore expects the UV buffers to be direct or will assert in transformDisplayUvCoords.
+ cameraUvCoords = createCameraUVBuffer();
+ transformedCameraUvCoords = createCameraUVBuffer();
+
+ FloatBuffer vertexBufferData = FloatBuffer.allocate(CAMERA_VERTICES.length);
+ vertexBufferData.put(CAMERA_VERTICES);
+
+ cameraVertexBuffer =
+ new Builder()
+ .vertexCount(VERTEX_COUNT)
+ .bufferCount(2)
+ .attribute(
+ VertexAttribute.POSITION,
+ 0,
+ VertexBuffer.AttributeType.FLOAT3,
+ 0,
+ (CAMERA_VERTICES.length / VERTEX_COUNT) * FLOAT_SIZE_IN_BYTES)
+ .attribute(
+ VertexAttribute.UV0,
+ 1,
+ VertexBuffer.AttributeType.FLOAT2,
+ 0,
+ (CAMERA_UVS.length / VERTEX_COUNT) * FLOAT_SIZE_IN_BYTES)
+ .build(engine.getFilamentEngine());
+
+ vertexBufferData.rewind();
+ Preconditions.checkNotNull(cameraVertexBuffer)
+ .setBufferAt(engine.getFilamentEngine(), POSITION_BUFFER_INDEX, vertexBufferData);
+
+ adjustCameraUvsForOpenGL();
+ cameraVertexBuffer.setBufferAt(
+ engine.getFilamentEngine(), UV_BUFFER_INDEX, transformedCameraUvCoords);
+
+ CompletableFuture materialFuture =
+ Material.builder()
+ .setSource(
+ renderer.getContext(),
+ RenderingResources.GetSceneformResource(
+ renderer.getContext(), RenderingResources.Resource.CAMERA_MATERIAL))
+ .build();
+
+ materialFuture
+ .thenAccept(
+ material -> {
+ defaultCameraMaterial = material;
+
+ // Only set the camera material if it hasn't already been set to a custom material.
+ if (cameraMaterial == null) {
+ setCameraMaterial(defaultCameraMaterial);
+ }
+ })
+ .exceptionally(
+ throwable -> {
+ Log.e(TAG, "Unable to load camera stream materials.", throwable);
+ return null;
+ });
+ }
+
+ public boolean isTextureInitialized() {
+ return isTextureInitialized;
+ }
+
+ public void initializeTexture(Frame frame) {
+ if (isTextureInitialized()) {
+ return;
+ }
+
+ Camera arCamera = frame.getCamera();
+ CameraIntrinsics intrinsics = arCamera.getTextureIntrinsics();
+ int[] dimensions = intrinsics.getImageDimensions();
+ int width = dimensions[0];
+ int height = dimensions[1];
+
+ cameraTexture = new ExternalTexture(cameraTextureId, width, height);
+
+ isTextureInitialized = true;
+
+ // If the camera material has already been set, call setCameraMaterial again to finish setup
+ // now that the CameraTexture has been created.
+ if (cameraMaterial != null) {
+ setCameraMaterial(cameraMaterial);
+ }
+ }
+
+ public void recalculateCameraUvs(Frame frame) {
+ IEngine engine = EngineInstance.getEngine();
+
+ FloatBuffer cameraUvCoords = this.cameraUvCoords;
+ FloatBuffer transformedCameraUvCoords = this.transformedCameraUvCoords;
+ VertexBuffer cameraVertexBuffer = this.cameraVertexBuffer;
+ frame.transformDisplayUvCoords(cameraUvCoords, transformedCameraUvCoords);
+ adjustCameraUvsForOpenGL();
+ cameraVertexBuffer.setBufferAt(
+ engine.getFilamentEngine(), UV_BUFFER_INDEX, transformedCameraUvCoords);
+ }
+
+ public void setCameraMaterial(Material material) {
+ cameraMaterial = material;
+
+ // The ExternalTexture can't be created until we receive the first AR Core Frame so that we
+ // can access the width and height of the camera texture. Return early if the ExternalTexture
+ // hasn't been created yet so we don't start rendering until we have a valid texture. This will
+ // be called again when the ExternalTexture is created.
+ if (!isTextureInitialized()) {
+ return;
+ }
+
+ material.setExternalTexture(MATERIAL_CAMERA_TEXTURE, Preconditions.checkNotNull(cameraTexture));
+
+ if (cameraStreamRenderable == UNINITIALIZED_FILAMENT_RENDERABLE) {
+ initializeFilamentRenderable();
+ } else {
+ RenderableManager renderableManager = EngineInstance.getEngine().getRenderableManager();
+ int renderableInstance = renderableManager.getInstance(cameraStreamRenderable);
+ renderableManager.setMaterialInstanceAt(
+ renderableInstance, 0, material.getFilamentMaterialInstance());
+ }
+ }
+
+ public void setCameraMaterialToDefault() {
+ if (defaultCameraMaterial != null) {
+ setCameraMaterial(defaultCameraMaterial);
+ } else {
+ // Default camera material hasn't been loaded yet, so just remove any custom material
+ // that has been set.
+ cameraMaterial = null;
+ }
+ }
+
+ public void setRenderPriority(int priority) {
+ renderablePriority = priority;
+ if (cameraStreamRenderable != UNINITIALIZED_FILAMENT_RENDERABLE) {
+ RenderableManager renderableManager = EngineInstance.getEngine().getRenderableManager();
+ int renderableInstance = renderableManager.getInstance(cameraStreamRenderable);
+ renderableManager.setPriority(renderableInstance, renderablePriority);
+ }
+ }
+
+ public int getRenderPriority() {
+ return renderablePriority;
+ }
+
+ private void adjustCameraUvsForOpenGL() {
+ // Correct for vertical coordinates to match OpenGL
+ for (int i = 1; i < VERTEX_COUNT * 2; i += 2) {
+ transformedCameraUvCoords.put(i, 1.0f - transformedCameraUvCoords.get(i));
+ }
+ }
+
+ private static FloatBuffer createCameraUVBuffer() {
+ FloatBuffer buffer =
+ ByteBuffer.allocateDirect(CAMERA_UVS.length * FLOAT_SIZE_IN_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ buffer.put(CAMERA_UVS);
+ buffer.rewind();
+
+ return buffer;
+ }
+
+ private void initializeFilamentRenderable() {
+ // create entity id
+ cameraStreamRenderable = EntityManager.get().create();
+
+ // create the quad renderable (leave off the aabb)
+ RenderableManager.Builder builder = new RenderableManager.Builder(1);
+ builder
+ .castShadows(false)
+ .receiveShadows(false)
+ .culling(false)
+ // Always draw the camera feed last to avoid overdraw
+ .priority(renderablePriority)
+ .geometry(
+ 0, RenderableManager.PrimitiveType.TRIANGLES, cameraVertexBuffer, cameraIndexBuffer)
+ .material(0, Preconditions.checkNotNull(cameraMaterial).getFilamentMaterialInstance())
+ .build(EngineInstance.getEngine().getFilamentEngine(), cameraStreamRenderable);
+
+ // add to the scene
+ scene.addEntity(cameraStreamRenderable);
+
+ ResourceManager.getInstance()
+ .getCameraStreamCleanupRegistry()
+ .register(
+ this,
+ new CleanupCallback(
+ scene, cameraStreamRenderable, cameraIndexBuffer, cameraVertexBuffer));
+ }
+
+ /** Cleanup filament objects after garbage collection */
+ private static final class CleanupCallback implements Runnable {
+ private final Scene scene;
+ private final int cameraStreamRenderable;
+ private final IndexBuffer cameraIndexBuffer;
+ private final VertexBuffer cameraVertexBuffer;
+
+ CleanupCallback(
+ Scene scene,
+ int cameraStreamRenderable,
+ IndexBuffer cameraIndexBuffer,
+ VertexBuffer cameraVertexBuffer) {
+ this.scene = scene;
+ this.cameraStreamRenderable = cameraStreamRenderable;
+ this.cameraIndexBuffer = cameraIndexBuffer;
+ this.cameraVertexBuffer = cameraVertexBuffer;
+ }
+
+ @Override
+ public void run() {
+ AndroidPreconditions.checkUiThread();
+
+ IEngine engine = EngineInstance.getEngine();
+ if (engine == null && !engine.isValid()) {
+ return;
+ }
+
+ if (cameraStreamRenderable != UNINITIALIZED_FILAMENT_RENDERABLE) {
+ scene.remove(cameraStreamRenderable);
+ }
+
+ engine.destroyIndexBuffer(cameraIndexBuffer);
+ engine.destroyVertexBuffer(cameraVertexBuffer);
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CleanupItem.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CleanupItem.java
new file mode 100644
index 0000000..f95b122
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CleanupItem.java
@@ -0,0 +1,28 @@
+package com.google.ar.sceneform.rendering;
+
+/**
+ * Runs a {@link Runnable} when a registered object is destroyed.
+ *
+ * For each object of type {@code T} registered, a {@link CleanupItem} will be created. The
+ * registered object's lifecycle will be tracked and when it is disposed the given {@link Runnable}
+ * will be run.
+ */
+class CleanupItem extends java.lang.ref.PhantomReference {
+ private final Runnable cleanupCallback;
+
+ /**
+ * @param trackedObject The object to be tracked until garbage collection
+ * @param referenceQueue The getFilamentEngine reference tracking mechanism
+ * @param cleanupCallback {@link Runnable} to be called once {@code trackedObject} is disposed.
+ */
+ CleanupItem(
+ T trackedObject, java.lang.ref.ReferenceQueue referenceQueue, Runnable cleanupCallback) {
+ super(trackedObject, referenceQueue);
+ this.cleanupCallback = cleanupCallback;
+ }
+
+ /** Executes the {@link Runnable}. */
+ void run() {
+ cleanupCallback.run();
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CleanupRegistry.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CleanupRegistry.java
new file mode 100644
index 0000000..1764545
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/CleanupRegistry.java
@@ -0,0 +1,67 @@
+package com.google.ar.sceneform.rendering;
+
+import com.google.ar.sceneform.resources.ResourceHolder;
+import java.lang.ref.ReferenceQueue;
+import java.util.HashSet;
+import java.util.Iterator;
+
+/**
+ * Maintains a {@link ReferenceQueue} and executes a {@link Runnable} after each object in the queue
+ * is garbage collected.
+ */
+public class CleanupRegistry implements ResourceHolder {
+
+ private final java.util.HashSet> cleanupItemHashSet;
+ private final ReferenceQueue referenceQueue;
+
+ public CleanupRegistry() {
+ this(new HashSet<>(), new ReferenceQueue<>());
+ }
+
+ public CleanupRegistry(
+ java.util.HashSet> cleanupItemHashSet, ReferenceQueue referenceQueue) {
+ this.cleanupItemHashSet = cleanupItemHashSet;
+ this.referenceQueue = referenceQueue;
+ }
+
+ /**
+ * Adds {@code trackedOBject} to the {@link ReferenceQueue}.
+ *
+ * @param trackedObject The target to be tracked.
+ * @param cleanupCallback Will be called after {@code trackedOBject} is disposed.
+ */
+ public void register(T trackedObject, Runnable cleanupCallback) {
+ cleanupItemHashSet.add(new CleanupItem(trackedObject, referenceQueue, cleanupCallback));
+ }
+
+ /**
+ * Polls the {@link ReferenceQueue} for garbage collected objects and runs the associated {@link
+ * Runnable}
+ *
+ * @return count of resources remaining.
+ */
+ @Override
+ @SuppressWarnings("unchecked") // safe cast from Reference to a CleanupItem
+ public long reclaimReleasedResources() {
+ CleanupItem ref = (CleanupItem) referenceQueue.poll();
+ while (ref != null) {
+ if (cleanupItemHashSet.contains(ref)) {
+ ref.run();
+ cleanupItemHashSet.remove(ref);
+ }
+ ref = (CleanupItem) referenceQueue.poll();
+ }
+ return cleanupItemHashSet.size();
+ }
+
+ /** Ignores reference count and releases any associated resources */
+ @Override
+ public void destroyAllResources() {
+ Iterator> iterator = cleanupItemHashSet.iterator();
+ while (iterator.hasNext()) {
+ CleanupItem ref = iterator.next();
+ iterator.remove();
+ ref.run();
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Color.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Color.java
new file mode 100644
index 0000000..8cc3254
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Color.java
@@ -0,0 +1,111 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.ColorInt;
+import com.google.android.filament.Colors;
+
+/**
+ * An RGBA color. Each component is a value with a range from 0 to 1. Can be created from an Android
+ * ColorInt.
+ */
+public class Color {
+ private static final float INT_COLOR_SCALE = 1.0f / 255.0f;
+
+ public float r;
+ public float g;
+ public float b;
+ public float a;
+
+ /** Construct a Color and default it to white (1, 1, 1, 1). */
+ @SuppressWarnings("initialization")
+ public Color() {
+ setWhite();
+ }
+
+ /** Construct a Color with the values of another color. */
+ @SuppressWarnings("initialization")
+ public Color(Color color) {
+ set(color);
+ }
+
+ /** Construct a color with the RGB values passed in and an alpha of 1. */
+ @SuppressWarnings("initialization")
+ public Color(float r, float g, float b) {
+ set(r, g, b);
+ }
+
+ /** Construct a color with the RGBA values passed in. */
+ @SuppressWarnings("initialization")
+ public Color(float r, float g, float b, float a) {
+ set(r, g, b, a);
+ }
+
+ /**
+ * Construct a color with an integer in the sRGB color space packed as an ARGB value. Used for
+ * constructing from an Android ColorInt.
+ */
+ @SuppressWarnings("initialization")
+ public Color(@ColorInt int argb) {
+ set(argb);
+ }
+
+ /** Set to the values of another color. */
+ public void set(Color color) {
+ set(color.r, color.g, color.b, color.a);
+ }
+
+ /** Set to the RGB values passed in and an alpha of 1. */
+ public void set(float r, float g, float b) {
+ set(r, g, b, 1.0f);
+ }
+
+ /** Set to the RGBA values passed in. */
+ public void set(float r, float g, float b, float a) {
+ this.r = Math.max(0.0f, Math.min(1.0f, r));
+ this.g = Math.max(0.0f, Math.min(1.0f, g));
+ this.b = Math.max(0.0f, Math.min(1.0f, b));
+ this.a = Math.max(0.0f, Math.min(1.0f, a));
+ }
+
+ /**
+ * Set to RGBA values from an integer in the sRGB color space packed as an ARGB value. Used for
+ * setting from an Android ColorInt.
+ */
+ public void set(@ColorInt int argb) {
+ // sRGB color
+ final int red = android.graphics.Color.red(argb);
+ final int green = android.graphics.Color.green(argb);
+ final int blue = android.graphics.Color.blue(argb);
+ final int alpha = android.graphics.Color.alpha(argb);
+
+ // Convert from sRGB to linear and from int to float.
+ float[] linearColor =
+ Colors.toLinear(
+ Colors.RgbType.SRGB,
+ (float) red * INT_COLOR_SCALE,
+ (float) green * INT_COLOR_SCALE,
+ (float) blue * INT_COLOR_SCALE);
+
+ r = linearColor[0];
+ g = linearColor[1];
+ b = linearColor[2];
+ a = (float) alpha * INT_COLOR_SCALE;
+ }
+
+ /** Sets the color to white. RGBA is (1, 1, 1, 1). */
+ private void setWhite() {
+ set(1.0f, 1.0f, 1.0f);
+ }
+
+ /** Returns a new color with Sceneform's tonemapping inversed. */
+ public Color inverseTonemap() {
+ Color color = new Color(r, g, b, a);
+ color.r = inverseTonemap(r);
+ color.g = inverseTonemap(g);
+ color.b = inverseTonemap(b);
+ return color;
+ }
+
+ private static float inverseTonemap(float val) {
+ return (val * -0.155f) / (val - 1.019f);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/DpToMetersViewSizer.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/DpToMetersViewSizer.java
new file mode 100644
index 0000000..1df2985
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/DpToMetersViewSizer.java
@@ -0,0 +1,55 @@
+package com.google.ar.sceneform.rendering;
+
+import android.view.View;
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Controls the size of a {@link ViewRenderable} in a {@link com.google.ar.sceneform.Scene} by
+ * defining how many dp (density-independent pixels) there are per meter. This is recommended when
+ * using an android layout that is built using dp.
+ *
+ * @see ViewRenderable.Builder#setSizer(ViewSizer)
+ * @see ViewRenderable#setSizer(ViewSizer)
+ */
+
+public class DpToMetersViewSizer implements ViewSizer {
+ private final int dpPerMeters;
+
+ // Defaults to zero, Z value of the size doesn't currently have any semantic meaning,
+ // but we may add that in later if we support ViewRenderables that have depth.
+ private static final float DEFAULT_SIZE_Z = 0.0f;
+
+ /**
+ * Constructor for creating a sizer for controlling the size of a {@link ViewRenderable} by
+ * defining how many dp there are per meter.
+ *
+ * @param dpPerMeters a number greater than zero representing the ratio of dp to meters
+ */
+ public DpToMetersViewSizer(int dpPerMeters) {
+ if (dpPerMeters <= 0) {
+ throw new IllegalArgumentException("dpPerMeters must be greater than zero.");
+ }
+
+ this.dpPerMeters = dpPerMeters;
+ }
+
+ /**
+ * Returns the number of dp (density-independent pixels) there are per meter that is used for
+ * controlling the size of a {@link ViewRenderable}.
+ */
+ public int getDpPerMeters() {
+ return dpPerMeters;
+ }
+
+ @Override
+ public Vector3 getSize(View view) {
+ Preconditions.checkNotNull(view, "Parameter \"view\" was null.");
+
+ float widthDp = ViewRenderableHelpers.convertPxToDp(view.getWidth());
+ float heightDp = ViewRenderableHelpers.convertPxToDp(view.getHeight());
+
+ return new Vector3(widthDp / dpPerMeters, heightDp / dpPerMeters, DEFAULT_SIZE_Z);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/EngineInstance.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/EngineInstance.java
new file mode 100644
index 0000000..0874906
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/EngineInstance.java
@@ -0,0 +1,163 @@
+package com.google.ar.sceneform.rendering;
+
+import android.opengl.EGLContext;
+import androidx.annotation.Nullable;
+import com.google.android.filament.Engine;
+import com.google.android.filament.Filament;
+import com.google.android.filament.gltfio.Gltfio;
+
+
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Store a single Filament Engine instance.
+ *
+ * @hide
+ */
+public class EngineInstance {
+ @Nullable private static IEngine engine = null;
+ @Nullable private static EGLContext glContext = null;
+ private static boolean headlessEngine = false;
+ private static boolean filamentInitialized = false;
+
+ public static void enableHeadlessEngine() {
+ headlessEngine = true;
+ }
+
+ public static void disableHeadlessEngine() {
+ headlessEngine = false;
+ }
+
+ public static boolean isHeadlessMode() {
+ return headlessEngine;
+ }
+
+ /**
+ * Get the Filament Engine instance, creating it if necessary.
+ *
+ * @throws IllegalStateException
+ */
+ public static IEngine getEngine() {
+ if (!headlessEngine) {
+ createEngine();
+ } else {
+ createHeadlessEngine();
+ }
+ if (engine == null) {
+ throw new IllegalStateException("Filament Engine creation has failed.");
+ }
+ return engine;
+ }
+
+
+ private static Engine createSharedFilamentEngine() {return null;}
+
+
+
+
+
+
+
+ private static Engine createFilamentEngine() {
+ Engine result = createSharedFilamentEngine();
+ if (result == null) {
+ glContext = GLHelper.makeContext();
+ result = Engine.create(glContext);
+ }
+ return result;
+ }
+
+
+ private static boolean destroySharedFilamentEngine() {return false;}
+
+
+
+
+ private static void destroyFilamentEngine() {
+ if (engine != null) {
+ if (headlessEngine || !destroySharedFilamentEngine()) {
+ if (glContext != null) {
+ GLHelper.destroyContext(glContext);
+ glContext = null;
+ }
+ Preconditions.checkNotNull(engine).destroy();
+ }
+ engine = null;
+ }
+ }
+
+
+ private static boolean loadUnifiedJni() {return false;}
+
+
+
+
+ private static void gltfioInit() {
+ Gltfio.init();
+ filamentInitialized = true;
+ }
+
+ /**
+ * Create the engine and GL Context if they have not been created yet.
+ *
+ * @throws IllegalStateException
+ */
+ private static void createEngine() {
+ if (engine == null) {
+
+ if (!filamentInitialized) {
+ try {
+ gltfioInit();
+ } catch (UnsatisfiedLinkError err) {
+ // Fallthrough and allow regular Filament to initialize.
+ }
+ }
+ if (!filamentInitialized) {
+ try {
+ Filament.init();
+ filamentInitialized = true;
+ } catch (UnsatisfiedLinkError err) {
+ // For Scene Viewer Filament's jni is included in another lib, try that before failing.
+ if (loadUnifiedJni()) {
+ filamentInitialized = true;
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ engine = new FilamentEngineWrapper(createFilamentEngine());
+
+ // Validate that the Engine and GL Context are valid.
+ if (engine == null) {
+ throw new IllegalStateException("Filament Engine creation has failed.");
+ }
+ }
+ }
+
+ /** Create a Swiftshader engine for testing. */
+ private static void createHeadlessEngine() {
+ if (engine == null) {
+ try {
+ engine = new HeadlessEngineWrapper();
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException("Filament Engine creation failed due to reflection error", e);
+ }
+ if (engine == null) {
+ throw new IllegalStateException("Filament Engine creation has failed.");
+ }
+ }
+ }
+
+ public static void destroyEngine() {
+ destroyFilamentEngine();
+ }
+
+ public static boolean isEngineDestroyed() {
+ return engine == null;
+ }
+
+ private static native Object nCreateEngine();
+
+ private static native void nDestroyEngine();
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/EnvironmentalHdrLightEstimate.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/EnvironmentalHdrLightEstimate.java
new file mode 100644
index 0000000..5d6d04c
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/EnvironmentalHdrLightEstimate.java
@@ -0,0 +1,157 @@
+package com.google.ar.sceneform.rendering;
+
+import android.media.Image;
+import android.media.Image.Plane;
+import androidx.annotation.Nullable;
+import com.google.ar.core.annotations.UsedByReflection;
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+
+/**
+ * Serialization structure for saving light estimate state for offline use, e.g. in tests.
+ *
+ * @hide
+ */
+public class EnvironmentalHdrLightEstimate implements Serializable {
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ static class CubeMapImage implements Serializable {
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ static class CubeMapPlane implements Serializable {
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ final int pixelStride;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ final int rowStride;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ final byte[] bytes;
+
+ public CubeMapPlane(Plane plane) {
+ ByteBuffer rgbaBuffer = plane.getBuffer();
+ bytes = new byte[rgbaBuffer.remaining()];
+ rgbaBuffer.get(bytes);
+ pixelStride = plane.getPixelStride();
+ rowStride = plane.getRowStride();
+ }
+ }
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ final int format;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ final CubeMapPlane[] planes;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ final int height;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ final int width;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ final long timestamp;
+
+ CubeMapImage(Image image) {
+ format = image.getFormat();
+ Plane[] imagePlanes = image.getPlanes();
+ planes = new CubeMapPlane[imagePlanes.length];
+ for (int i = 0; i < imagePlanes.length; ++i) {
+ planes[i] = new CubeMapPlane(imagePlanes[i]);
+ }
+ height = image.getHeight();
+ width = image.getWidth();
+ timestamp = image.getTimestamp();
+ }
+ }
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ @Nullable
+ private final float[] sphericalHarmonics;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ @Nullable
+ private final float[] direction;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ private final float colorR;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ private final float colorG;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ private final float colorB;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ private final float colorA;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ private final float relativeIntensity;
+
+ @UsedByReflection("EnvironmentalHdrLightEstimate.java")
+ @Nullable
+ private final CubeMapImage[] cubeMap;
+
+ // incompatible types in argument.
+ // incompatible types in assignment.
+ @SuppressWarnings({
+ "nullness:argument.type.incompatible",
+ "nullness:assignment.type.incompatible"
+ })
+ public EnvironmentalHdrLightEstimate(
+ @Nullable float[] sphericalHarmonics,
+ @Nullable float[] direction,
+ Color colorCorrection,
+ float relativeIntensity,
+ @Nullable Image[] cubeMap) {
+ if (sphericalHarmonics != null) {
+ this.sphericalHarmonics = new float[sphericalHarmonics.length];
+ System.arraycopy(
+ sphericalHarmonics, 0, this.sphericalHarmonics, 0, sphericalHarmonics.length);
+ } else {
+ this.sphericalHarmonics = null;
+ }
+ if (direction != null) {
+ this.direction = new float[direction.length];
+ System.arraycopy(direction, 0, this.direction, 0, direction.length);
+ } else {
+ this.direction = null;
+ }
+ colorR = colorCorrection.r;
+ colorG = colorCorrection.g;
+ colorB = colorCorrection.b;
+ colorA = colorCorrection.a;
+ this.relativeIntensity = relativeIntensity;
+ if (cubeMap != null) {
+ this.cubeMap = new CubeMapImage[cubeMap.length];
+ for (int i = 0; i < cubeMap.length; ++i) {
+ this.cubeMap[i] = new CubeMapImage(cubeMap[i]);
+ }
+ } else {
+ this.cubeMap = null;
+ }
+ }
+
+ @Nullable
+ public float[] getSphericalHarmonics() {
+ return sphericalHarmonics;
+ }
+
+ @Nullable
+ public float[] getDirection() {
+ return direction;
+ }
+
+ public Color getColor() {
+ return new Color(colorR, colorG, colorB, colorA);
+ }
+
+ public float getRelativeIntensity() {
+ return relativeIntensity;
+ }
+
+ // incompatible types in return.
+ @SuppressWarnings("nullness:return.type.incompatible")
+ @Nullable
+ public CubeMapImage[] getCubeMap() {
+ return cubeMap;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ExternalTexture.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ExternalTexture.java
new file mode 100644
index 0000000..d433218
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ExternalTexture.java
@@ -0,0 +1,144 @@
+package com.google.ar.sceneform.rendering;
+
+import android.graphics.SurfaceTexture;
+import android.view.Surface;
+import androidx.annotation.Nullable;
+import com.google.android.filament.Stream;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Creates an Android {@link SurfaceTexture} and {@link Surface} that can be displayed by Sceneform.
+ * Useful for displaying video, or anything else that can be drawn to a {@link SurfaceTexture}.
+ *
+ * The getFilamentEngine OpenGL ES texture is automatically created by Sceneform. Also, {@link
+ * SurfaceTexture#updateTexImage()} is automatically called and should not be called manually.
+ *
+ *
Call {@link Material#setExternalTexture(String, ExternalTexture)} to use an ExternalTexture.
+ * The material parameter MUST be of type 'samplerExternal'.
+ */
+public class ExternalTexture {
+ private static final String TAG = ExternalTexture.class.getSimpleName();
+
+ @Nullable private final SurfaceTexture surfaceTexture;
+ @Nullable private final Surface surface;
+
+ @Nullable private com.google.android.filament.Texture filamentTexture;
+ @Nullable private Stream filamentStream;
+
+ /** Creates an ExternalTexture with a new Android {@link SurfaceTexture} and {@link Surface}. */
+ @SuppressWarnings("initialization")
+ public ExternalTexture() {
+ // Create the Android surface texture.
+ SurfaceTexture surfaceTexture = new SurfaceTexture(0);
+ surfaceTexture.detachFromGLContext();
+ this.surfaceTexture = surfaceTexture;
+
+ // Create the Android surface.
+ surface = new Surface(surfaceTexture);
+
+ // Create the filament stream.
+ Stream stream =
+ new Stream.Builder()
+ .stream(surfaceTexture).build(EngineInstance.getEngine().getFilamentEngine());
+
+ initialize(stream);
+ }
+
+ /**
+ * Creates an ExternalTexture from an OpenGL ES textureId without a SurfaceTexture. For internal
+ * use only.
+ */
+ @SuppressWarnings("initialization")
+ ExternalTexture(int textureId, int width, int height) {
+ // Explicitly set the surface and surfaceTexture to null, since they are unused in this case.
+ surfaceTexture = null;
+ surface = null;
+
+ // Create the filament stream.
+ Stream stream =
+ new Stream.Builder()
+ .stream(textureId)
+ .width(width)
+ .height(height)
+ .build(EngineInstance.getEngine().getFilamentEngine());
+
+ initialize(stream);
+ }
+
+ /** Gets the surface texture created for this ExternalTexture. */
+ public SurfaceTexture getSurfaceTexture() {
+ return Preconditions.checkNotNull(surfaceTexture);
+ }
+
+ /**
+ * Gets the surface created for this ExternalTexture that draws to {@link #getSurfaceTexture()}
+ */
+ public Surface getSurface() {
+ return Preconditions.checkNotNull(surface);
+ }
+
+ com.google.android.filament.Texture getFilamentTexture() {
+ return Preconditions.checkNotNull(filamentTexture);
+ }
+
+ Stream getFilamentStream() {
+ return Preconditions.checkNotNull(filamentStream);
+ }
+
+ @SuppressWarnings("initialization")
+ private void initialize(Stream filamentStream) {
+ if (filamentTexture != null) {
+ throw new AssertionError("Stream was initialized twice");
+ }
+
+ // Create the filament stream.
+ IEngine engine = EngineInstance.getEngine();
+ this.filamentStream = filamentStream;
+
+ // Create the filament texture.
+ final com.google.android.filament.Texture.Sampler textureSampler =
+ com.google.android.filament.Texture.Sampler.SAMPLER_EXTERNAL;
+ final com.google.android.filament.Texture.InternalFormat textureInternalFormat =
+ com.google.android.filament.Texture.InternalFormat.RGB8;
+
+ filamentTexture =
+ new com.google.android.filament.Texture.Builder()
+ .sampler(textureSampler)
+ .format(textureInternalFormat)
+ .build(engine.getFilamentEngine());
+
+ filamentTexture.setExternalStream(engine.getFilamentEngine(), filamentStream);
+ ResourceManager.getInstance()
+ .getExternalTextureCleanupRegistry()
+ .register(this, new CleanupCallback(filamentTexture, filamentStream));
+ }
+
+ /** Cleanup filament objects after garbage collection */
+ private static final class CleanupCallback implements Runnable {
+ @Nullable private final com.google.android.filament.Texture filamentTexture;
+ @Nullable private final Stream filamentStream;
+
+ CleanupCallback(com.google.android.filament.Texture filamentTexture, Stream filamentStream) {
+ this.filamentTexture = filamentTexture;
+ this.filamentStream = filamentStream;
+ }
+
+ @Override
+ public void run() {
+ AndroidPreconditions.checkUiThread();
+
+ IEngine engine = EngineInstance.getEngine();
+ if (engine == null || !engine.isValid()) {
+ return;
+ }
+ if (filamentTexture != null) {
+ engine.destroyTexture(filamentTexture);
+ }
+
+ if (filamentStream != null) {
+ engine.destroyStream(filamentStream);
+ }
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FilamentEngineWrapper.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FilamentEngineWrapper.java
new file mode 100644
index 0000000..0dfcc42
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FilamentEngineWrapper.java
@@ -0,0 +1,183 @@
+package com.google.ar.sceneform.rendering;
+
+import com.google.android.filament.Camera;
+import com.google.android.filament.Engine;
+import com.google.android.filament.Fence;
+import com.google.android.filament.IndexBuffer;
+import com.google.android.filament.IndirectLight;
+import com.google.android.filament.LightManager;
+import com.google.android.filament.MaterialInstance;
+import com.google.android.filament.NativeSurface;
+import com.google.android.filament.RenderableManager;
+import com.google.android.filament.Scene;
+import com.google.android.filament.Skybox;
+import com.google.android.filament.Stream;
+import com.google.android.filament.SwapChain;
+import com.google.android.filament.TransformManager;
+import com.google.android.filament.VertexBuffer;
+import com.google.android.filament.View;
+
+/** Wraps calls to Filament engine. */
+public class FilamentEngineWrapper implements IEngine {
+
+ final Engine engine;
+
+ public FilamentEngineWrapper(Engine engine) {
+ this.engine = engine;
+ }
+
+ @Override
+ public Engine getFilamentEngine() {
+ return engine;
+ }
+
+ @Override
+ public boolean isValid() {
+ return engine.isValid();
+ }
+
+ @Override
+ public void destroy() {
+ engine.destroy();
+ }
+
+ @Override
+ public SwapChain createSwapChain(Object surface) {
+ return engine.createSwapChain(surface);
+ }
+
+ @Override
+ public SwapChain createSwapChain(Object surface, long flags) {
+ return engine.createSwapChain(surface, flags);
+ }
+
+ @Override
+ public SwapChain createSwapChainFromNativeSurface(NativeSurface surface, long flags) {
+ return engine.createSwapChainFromNativeSurface(surface, flags);
+ }
+
+ @Override
+ public void destroySwapChain(SwapChain swapChain) {
+ engine.destroySwapChain(swapChain);
+ }
+
+ @Override
+ public View createView() {
+ return engine.createView();
+ }
+
+ @Override
+ public void destroyView(View view) {
+ engine.destroyView(view);
+ }
+
+ @Override
+ public com.google.android.filament.Renderer createRenderer() {
+ return engine.createRenderer();
+ }
+
+ @Override
+ public void destroyRenderer(com.google.android.filament.Renderer renderer) {
+ engine.destroyRenderer(renderer);
+ }
+
+ @Override
+ public Camera createCamera() {
+ return engine.createCamera();
+ }
+
+ @Override
+ public Camera createCamera(int entity) {
+ return engine.createCamera(entity);
+ }
+
+ @Override
+ public void destroyCamera(Camera camera) {
+ engine.destroyCamera(camera);
+ }
+
+ @Override
+ public Scene createScene() {
+ return engine.createScene();
+ }
+
+ @Override
+ public void destroyScene(Scene scene) {
+ engine.destroyScene(scene);
+ }
+
+ @Override
+ public void destroyStream(Stream stream) {
+ engine.destroyStream(stream);
+ }
+
+ @Override
+ public Fence createFence() {
+ return engine.createFence();
+ }
+
+ @Override
+ public void destroyFence(Fence fence) {
+ engine.destroyFence(fence);
+ }
+
+ @Override
+ public void destroyIndexBuffer(IndexBuffer indexBuffer) {
+ engine.destroyIndexBuffer(indexBuffer);
+ }
+
+ @Override
+ public void destroyVertexBuffer(VertexBuffer vertexBuffer) {
+ engine.destroyVertexBuffer(vertexBuffer);
+ }
+
+ @Override
+ public void destroyIndirectLight(IndirectLight ibl) {
+ engine.destroyIndirectLight(ibl);
+ }
+
+ @Override
+ public void destroyMaterial(com.google.android.filament.Material material) {
+ engine.destroyMaterial(material);
+ }
+
+ @Override
+ public void destroyMaterialInstance(MaterialInstance materialInstance) {
+ engine.destroyMaterialInstance(materialInstance);
+ }
+
+ @Override
+ public void destroySkybox(Skybox skybox) {
+ engine.destroySkybox(skybox);
+ }
+
+ @Override
+ public void destroyTexture(com.google.android.filament.Texture texture) {
+ engine.destroyTexture(texture);
+ }
+
+ @Override
+ public void destroyEntity(int entity) {
+ engine.destroyEntity(entity);
+ }
+
+ @Override
+ public TransformManager getTransformManager() {
+ return engine.getTransformManager();
+ }
+
+ @Override
+ public LightManager getLightManager() {
+ return engine.getLightManager();
+ }
+
+ @Override
+ public RenderableManager getRenderableManager() {
+ return engine.getRenderableManager();
+ }
+
+ @Override
+ public void flushAndWait() {
+ engine.flushAndWait();
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FixedHeightViewSizer.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FixedHeightViewSizer.java
new file mode 100644
index 0000000..323998d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FixedHeightViewSizer.java
@@ -0,0 +1,55 @@
+package com.google.ar.sceneform.rendering;
+
+import android.view.View;
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Controls the size of a {@link ViewRenderable} in a {@link com.google.ar.sceneform.Scene} by
+ * defining how tall it should be in meters. The width will change to match the aspect ratio of the
+ * view.
+ *
+ * @see ViewRenderable.Builder#setSizer(ViewSizer)
+ * @see ViewRenderable#setSizer(ViewSizer)
+ */
+
+public class FixedHeightViewSizer implements ViewSizer {
+ private final float heightMeters;
+
+ // Defaults to zero, Z value of the size doesn't currently have any semantic meaning,
+ // but we may add that in later if we support ViewRenderables that have depth.
+ private static final float DEFAULT_SIZE_Z = 0.0f;
+
+ /**
+ * Constructor for creating a sizer for controlling the size of a {@link ViewRenderable} by
+ * defining a fixed height.
+ *
+ * @param heightMeters a number greater than zero representing the height in meters.
+ */
+ public FixedHeightViewSizer(float heightMeters) {
+ if (heightMeters <= 0) {
+ throw new IllegalArgumentException("heightMeters must be greater than zero.");
+ }
+
+ this.heightMeters = heightMeters;
+ }
+
+ /** Returns the height in meters used for controlling the size of a {@link ViewRenderable}. */
+ public float getHeight() {
+ return heightMeters;
+ }
+
+ @Override
+ public Vector3 getSize(View view) {
+ Preconditions.checkNotNull(view, "Parameter \"view\" was null.");
+
+ float aspectRatio = ViewRenderableHelpers.getAspectRatio(view);
+
+ if (aspectRatio == 0.0f) {
+ return Vector3.zero();
+ }
+
+ return new Vector3(heightMeters * aspectRatio, heightMeters, DEFAULT_SIZE_Z);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FixedWidthViewSizer.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FixedWidthViewSizer.java
new file mode 100644
index 0000000..78b133c
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FixedWidthViewSizer.java
@@ -0,0 +1,55 @@
+package com.google.ar.sceneform.rendering;
+
+import android.view.View;
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.Preconditions;
+
+/**
+ * Controls the size of a {@link ViewRenderable} in a {@link com.google.ar.sceneform.Scene} by
+ * defining how wide it should be in meters. The height will change to match the aspect ratio of the
+ * view.
+ *
+ * @see ViewRenderable.Builder#setSizer(ViewSizer)
+ * @see ViewRenderable#setSizer(ViewSizer)
+ */
+
+public class FixedWidthViewSizer implements ViewSizer {
+ private final float widthMeters;
+
+ // Defaults to zero, Z value of the size doesn't currently have any semantic meaning,
+ // but we may add that in later if we support ViewRenderables that have depth.
+ private static final float DEFAULT_SIZE_Z = 0.0f;
+
+ /**
+ * Constructor for creating a sizer for controlling the size of a {@link ViewRenderable} by
+ * defining a fixed width.
+ *
+ * @param widthMeters a number greater than zero representing the width in meters.
+ */
+ public FixedWidthViewSizer(float widthMeters) {
+ if (widthMeters <= 0) {
+ throw new IllegalArgumentException("widthMeters must be greater than zero.");
+ }
+
+ this.widthMeters = widthMeters;
+ }
+
+ /** Returns the width in meters used for controlling the size of a {@link ViewRenderable}. */
+ public float getWidth() {
+ return widthMeters;
+ }
+
+ @Override
+ public Vector3 getSize(View view) {
+ Preconditions.checkNotNull(view, "Parameter \"view\" was null.");
+
+ float aspectRatio = ViewRenderableHelpers.getAspectRatio(view);
+
+ if (aspectRatio == 0.0f) {
+ return Vector3.zero();
+ }
+
+ return new Vector3(widthMeters, widthMeters / aspectRatio, DEFAULT_SIZE_Z);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FutureHelper.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FutureHelper.java
new file mode 100644
index 0000000..6904d8c
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/FutureHelper.java
@@ -0,0 +1,33 @@
+package com.google.ar.sceneform.rendering;
+
+import android.util.Log;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+/** Prints error messages if needed. */
+@SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"}) // CompletableFuture
+class FutureHelper {
+ private FutureHelper() {}
+
+
+ /**
+ * Outputs a log message if input completes exceptionally.
+ *
+ *
Does not remove the exception from input. If some later handler is able to do more with the
+ * exception it is still possible.
+ *
+ * @param tag tag for the log message.
+ * @param input A completable future that may have failed.
+ * @param errorMsg Message to print along with the exception.
+ * @return input so that the function may be chained.
+ */
+ static CompletableFuture logOnException(
+ final String tag, final CompletableFuture input, final String errorMsg) {
+ input.exceptionally(
+ throwable -> {
+ Log.e(tag, errorMsg, throwable);
+ throw new CompletionException(throwable);
+ });
+ return input;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/GLHelper.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/GLHelper.java
new file mode 100644
index 0000000..29d7bf5
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/GLHelper.java
@@ -0,0 +1,75 @@
+package com.google.ar.sceneform.rendering;
+
+import android.opengl.EGL14;
+import android.opengl.EGLConfig;
+import android.opengl.EGLContext;
+import android.opengl.EGLDisplay;
+import android.opengl.EGLSurface;
+import android.opengl.GLES11Ext;
+import android.opengl.GLES30;
+
+/**
+ * Convenience class to perform common GL operations
+ *
+ * @hide
+ */
+public class GLHelper {
+ private static final String TAG = GLHelper.class.getSimpleName();
+
+ private static final int EGL_OPENGL_ES3_BIT = 0x40;
+
+ public static EGLContext makeContext() {
+ return makeContext(EGL14.EGL_NO_CONTEXT);
+ }
+
+ @SuppressWarnings("nullness")
+ public static EGLContext makeContext(EGLContext shareContext) {
+ EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+
+ int[] minorMajor = null;
+ EGL14.eglInitialize(display, minorMajor, 0, minorMajor, 0);
+ EGLConfig[] configs = new EGLConfig[1];
+ int[] numConfig = {0};
+ int[] attribs = {EGL14.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL14.EGL_NONE};
+ EGL14.eglChooseConfig(display, attribs, 0, configs, 0, 1, numConfig, 0);
+
+ int[] contextAttribs = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE};
+ EGLContext context =
+ EGL14.eglCreateContext(display, configs[0], shareContext, contextAttribs, 0);
+
+ int[] surfaceAttribs = {
+ EGL14.EGL_WIDTH, 1,
+ EGL14.EGL_HEIGHT, 1,
+ EGL14.EGL_NONE
+ };
+
+ EGLSurface surface = EGL14.eglCreatePbufferSurface(display, configs[0], surfaceAttribs, 0);
+
+ if (!EGL14.eglMakeCurrent(display, surface, surface, context)) {
+ throw new IllegalStateException("Error making GL context.");
+ }
+
+ return context;
+ }
+
+ public static int createCameraTexture() {
+ int[] textures = new int[1];
+ GLES30.glGenTextures(1, textures, 0);
+ int result = textures[0];
+
+ final int textureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
+ GLES30.glBindTexture(textureTarget, result);
+ GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
+ GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
+ GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
+ GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_NEAREST);
+ return result;
+ }
+
+ public static void destroyContext(EGLContext context) {
+ EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ if (!EGL14.eglDestroyContext(display, context)) {
+ throw new IllegalStateException("Error destroying GL context.");
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/HeadlessEngineWrapper.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/HeadlessEngineWrapper.java
new file mode 100644
index 0000000..a5da632
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/HeadlessEngineWrapper.java
@@ -0,0 +1,120 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.NonNull;
+import com.google.android.filament.Engine;
+import com.google.android.filament.NativeSurface;
+import com.google.android.filament.SwapChain;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+/** Interface for the swiftshader backed version of the Filament engine. */
+public class HeadlessEngineWrapper extends FilamentEngineWrapper {
+ public static final String TAG = HeadlessEngineWrapper.class.getName();
+
+ long nativeHandle;
+ private static final Constructor swapChainInit;
+ private static final Constructor engineInit;
+ private static final Method getNativeEngineMethod;
+ private static final Method getNativeSwapChainMethod;
+
+ static {
+ try {
+ getNativeSwapChainMethod = SwapChain.class.getDeclaredMethod("getNativeObject");
+ swapChainInit = SwapChain.class.getDeclaredConstructor(long.class, Object.class);
+ getNativeEngineMethod = Engine.class.getDeclaredMethod("getNativeObject");
+ engineInit = Engine.class.getDeclaredConstructor(long.class);
+ getNativeSwapChainMethod.setAccessible(true);
+ swapChainInit.setAccessible(true);
+ getNativeEngineMethod.setAccessible(true);
+ engineInit.setAccessible(true);
+ } catch (Exception e) {
+ throw new IllegalStateException("Couldn't get native getters", e);
+ }
+ }
+
+ public HeadlessEngineWrapper() throws ReflectiveOperationException {
+ super(engineInit.newInstance(nCreateSwiftShaderEngine()));
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ Long nativeEngineHandle = (Long) getNativeEngineMethod.invoke(engine);
+ nDestroySwiftShaderEngine(Preconditions.checkNotNull(nativeEngineHandle));
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public SwapChain createSwapChain(@NonNull Object surface) {
+ try {
+ Long nativeEngineHandle = (Long) getNativeEngineMethod.invoke(engine);
+ @SuppressWarnings("nullness:assignment.type.incompatible") // b/140537868
+ @NonNull
+ Object fakeSurface = null;
+ return swapChainInit.newInstance(
+ nCreateSwiftShaderSwapChain(Preconditions.checkNotNull(nativeEngineHandle), 0),
+ fakeSurface);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public SwapChain createSwapChain(@NonNull Object surface, long flags) {
+ try {
+ Long nativeEngineHandle = (Long) getNativeEngineMethod.invoke(engine);
+ @SuppressWarnings("nullness:assignment.type.incompatible") // b/140537868
+ @NonNull
+ Object fakeSurface = null;
+ return swapChainInit.newInstance(
+ nCreateSwiftShaderSwapChain(Preconditions.checkNotNull(nativeEngineHandle), flags),
+ fakeSurface);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public SwapChain createSwapChainFromNativeSurface(@NonNull NativeSurface surface, long flags) {
+ try {
+ Long nativeEngineHandle = (Long) getNativeEngineMethod.invoke(engine);
+ @SuppressWarnings("nullness:assignment.type.incompatible") // b/140537868
+ @NonNull
+ Object fakeSurface = null;
+ return swapChainInit.newInstance(
+ nCreateSwiftShaderSwapChain(Preconditions.checkNotNull(nativeEngineHandle), flags),
+ fakeSurface);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void destroySwapChain(@NonNull SwapChain swapChain) {
+ try {
+ Long nativeEngineHandle = (Long) getNativeEngineMethod.invoke(engine);
+ Long swapChainHandle = (Long) getNativeSwapChainMethod.invoke(swapChain);
+ nDestroySwiftShaderSwapChain(
+ Preconditions.checkNotNull(nativeEngineHandle),
+ Preconditions.checkNotNull(swapChainHandle));
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // LINT.IfChange(api)
+ private static native long nCreateSwiftShaderEngine();
+
+ private static native void nDestroySwiftShaderEngine(long nativeEngine);
+
+ private static native long nCreateSwiftShaderSwapChain(long nativeEngine, long flags);
+
+ private static native void nDestroySwiftShaderSwapChain(long nativeEngine, long nativeSwapChain);
+ // LINT.ThenChange(
+ //
+ // //depot/google3/third_party/arcore/ar/sceneform/viewer/swiftshader/platform_swiftshader_jni.cc:api
+ // )
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/IEngine.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/IEngine.java
new file mode 100644
index 0000000..b093b0e
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/IEngine.java
@@ -0,0 +1,113 @@
+package com.google.ar.sceneform.rendering;
+
+import com.google.android.filament.Camera;
+import com.google.android.filament.Engine;
+import com.google.android.filament.Entity;
+import com.google.android.filament.Fence;
+import com.google.android.filament.IndexBuffer;
+import com.google.android.filament.IndirectLight;
+import com.google.android.filament.LightManager;
+import com.google.android.filament.MaterialInstance;
+import com.google.android.filament.NativeSurface;
+import com.google.android.filament.RenderableManager;
+import com.google.android.filament.Scene;
+import com.google.android.filament.Skybox;
+import com.google.android.filament.Stream;
+import com.google.android.filament.SwapChain;
+import com.google.android.filament.TransformManager;
+import com.google.android.filament.VertexBuffer;
+import com.google.android.filament.View;
+
+/** Engine interface to support multiple flavors of the getFilamentEngine filament engine. */
+public interface IEngine {
+
+ Engine getFilamentEngine();
+
+ boolean isValid();
+
+ void destroy();
+
+ // SwapChain
+
+ /** Valid surface types: - Android: Surface - Other: none */
+ SwapChain createSwapChain(Object surface);
+
+ /**
+ * Valid surface types: - Android: Surface - Other: none
+ *
+ * Flags: see CONFIG flags in SwapChain.
+ *
+ * @see SwapChain#CONFIG_DEFAULT
+ * @see SwapChain#CONFIG_TRANSPARENT
+ * @see SwapChain#CONFIG_READABLE
+ */
+ SwapChain createSwapChain(Object surface, long flags);
+
+ SwapChain createSwapChainFromNativeSurface(NativeSurface surface, long flags);
+
+ void destroySwapChain(SwapChain swapChain);
+
+ // View
+
+ View createView();
+
+ void destroyView(View view);
+
+ // Renderer
+
+ com.google.android.filament.Renderer createRenderer();
+
+ void destroyRenderer(com.google.android.filament.Renderer renderer);
+
+ // Camera
+
+ Camera createCamera();
+
+ Camera createCamera(@Entity int entity);
+
+ void destroyCamera(Camera camera);
+
+ // Scene
+
+ Scene createScene();
+
+ void destroyScene(Scene scene);
+
+ // Stream
+
+ void destroyStream(Stream stream);
+
+ // Fence
+
+ Fence createFence();
+
+ void destroyFence(Fence fence);
+
+ // others...
+
+ void destroyIndexBuffer(IndexBuffer indexBuffer);
+
+ void destroyVertexBuffer(VertexBuffer vertexBuffer);
+
+ void destroyIndirectLight(IndirectLight ibl);
+
+ void destroyMaterial(com.google.android.filament.Material material);
+
+ void destroyMaterialInstance(MaterialInstance materialInstance);
+
+ void destroySkybox(Skybox skybox);
+
+ void destroyTexture(com.google.android.filament.Texture texture);
+
+ void destroyEntity(@Entity int entity);
+
+ // Managers
+
+ TransformManager getTransformManager();
+
+ LightManager getLightManager();
+
+ RenderableManager getRenderableManager();
+
+ void flushAndWait();
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/IRenderableInternalData.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/IRenderableInternalData.java
new file mode 100644
index 0000000..e189e04
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/IRenderableInternalData.java
@@ -0,0 +1,99 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.filament.Entity;
+import com.google.android.filament.IndexBuffer;
+import com.google.android.filament.VertexBuffer;
+
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.RenderableInternalData.MeshData;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+
+// TODO: Split IRenderableInternalData into RenderableInternalSfbData and
+// RenderableInternalDefinitionData
+interface IRenderableInternalData {
+
+ void setCenterAabb(Vector3 minAabb);
+
+ Vector3 getCenterAabb();
+
+ void setExtentsAabb(Vector3 maxAabb);
+
+ Vector3 getExtentsAabb();
+
+ Vector3 getSizeAabb();
+
+ void setTransformScale(float scale);
+
+ float getTransformScale();
+
+ void setTransformOffset(Vector3 offset);
+
+ Vector3 getTransformOffset();
+
+ ArrayList getMeshes();
+
+ void setIndexBuffer(@Nullable IndexBuffer indexBuffer);
+
+ @Nullable
+ IndexBuffer getIndexBuffer();
+
+ void setVertexBuffer(@Nullable VertexBuffer vertexBuffer);
+
+ @Nullable
+ VertexBuffer getVertexBuffer();
+
+ void setRawIndexBuffer(@Nullable IntBuffer rawIndexBuffer);
+
+ @Nullable
+ IntBuffer getRawIndexBuffer();
+
+ void setRawPositionBuffer(@Nullable FloatBuffer rawPositionBuffer);
+
+ @Nullable
+ FloatBuffer getRawPositionBuffer();
+
+ void setRawTangentsBuffer(@Nullable FloatBuffer rawTangentsBuffer);
+
+ @Nullable
+ FloatBuffer getRawTangentsBuffer();
+
+ void setRawUvBuffer(@Nullable FloatBuffer rawUvBuffer);
+
+ @Nullable
+ FloatBuffer getRawUvBuffer();
+
+ void setRawColorBuffer(@Nullable FloatBuffer rawColorBuffer);
+
+ @Nullable
+ FloatBuffer getRawColorBuffer();
+
+ void setAnimationNames(@NonNull List animationNames);
+
+ @NonNull
+ List getAnimationNames();
+
+
+
+
+
+
+
+
+
+
+
+ void buildInstanceData(Renderable renderable, @Entity int renderedEntity);
+ /**
+ * Removes any memory used by the object.
+ *
+ * @hide
+ */
+ void dispose();
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Light.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Light.java
new file mode 100644
index 0000000..dd581e8
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Light.java
@@ -0,0 +1,348 @@
+package com.google.ar.sceneform.rendering;
+
+import android.os.Build;
+import androidx.annotation.RequiresApi;
+import com.google.android.filament.Colors;
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import java.util.ArrayList;
+
+/** Light property store. */
+@RequiresApi(api = Build.VERSION_CODES.N)
+public class Light {
+ /** Type of Light Source */
+ public enum Type {
+ /**
+ * Approximates light radiating in all directions from a single point in space, where the
+ * intensity falls off with the inverse square of the distance. Point lights have a position but
+ * no direction. Use {@link #setFalloffRadius} to control the falloff.
+ */
+ POINT,
+ /** Approximates an infinitely far away, purely directional light */
+ DIRECTIONAL,
+ /**
+ * Similar to a point light but radiating light in a cone rather than all directions. Note that
+ * as you make the cone wider, the energy is spread causing the lighting to appear dimmer. A
+ * spotlight has a position and a direction. Use {@link #setInnerConeAngle} and {@link
+ * #setOuterConeAngle} to control the cone size.
+ */
+ SPOTLIGHT,
+ /**
+ * The same as a spotlight with the exception that the apparent lighting stays the same as the
+ * cone angle changes. A spotlight has a position and a direction. Use {@link
+ * #setInnerConeAngle} and {@link #setOuterConeAngle} to control the cone size.
+ */
+ FOCUSED_SPOTLIGHT
+ };
+
+ interface LightChangedListener {
+ void onChange();
+ };
+
+ /** Minimum accepted light intensity */
+ private static final float MIN_LIGHT_INTENSITY = 0.0001f;
+
+ private final Type type;
+ private final boolean enableShadows;
+
+ private Vector3 position;
+ private Vector3 direction;
+ private final Color color;
+ private float intensity;
+ private float falloffRadius;
+ private float spotlightConeInner;
+ private float spotlightConeOuter;
+
+ private final ArrayList changedListeners = new ArrayList<>();
+
+ /** Constructs a default light, if nothing else is set */
+ public static Builder builder(Type type) {
+ AndroidPreconditions.checkMinAndroidApiLevel();
+ return new Builder(type);
+ }
+
+ /**
+ * Sets the "RGB" color of the light. Note that intensity is a separate parameter, so you should
+ * set the pure color (i.e. each channel is in the [0,1] range). However setting values outside
+ * that range is valid.
+ *
+ * @param color "RGB" color, the default is 0xffffffff
+ */
+ public void setColor(Color color) {
+ this.color.set(color);
+ fireChangedListeners();
+ }
+
+ /**
+ * Sets the "RGB" color of the light based on the desired "color temperature."
+ *
+ * @param temperature color temperature in Kelvin on a scale from 1,000 to 10,000K. Typical
+ * commercial and residential lighting falls somewhere in the 2000K to 6500K range.
+ */
+ public void setColorTemperature(float temperature) {
+ final float[] rgbColor = Colors.cct(temperature);
+ setColor(new Color(rgbColor[0], rgbColor[1], rgbColor[2]));
+ }
+
+ /**
+ * Sets the light intensity which determines how bright the light is in Lux (lx) or Lumens (lm)
+ * (depending on the light type). Larger values produce brighter lights and near zero values
+ * generate very little light. A household light bulb will generally have an intensity between 800
+ * - 2500 lm whereas sunlight will be around 120,000 lx. There is no absolute upper bound but
+ * values larger than sunlight (120,000 lx) are generally not needed.
+ *
+ * @param intensity the intensity of the light, values greater than one are valid. The intensity
+ * will be clamped and cannot be zero or negative. For directional lights the default is 420
+ * lx. For other other lights the default is 2500 lm.
+ */
+ public void setIntensity(float intensity) {
+ this.intensity = Math.max(intensity, MIN_LIGHT_INTENSITY);
+ fireChangedListeners();
+ }
+
+ /**
+ * Sets the range that the light intensity falls off to zero. This has no affect on the {@link
+ * Light.Type#DIRECTIONAL} type.
+ *
+ * @param falloffRadius the light radius in world units, default is 10.0
+ */
+ public void setFalloffRadius(float falloffRadius) {
+ this.falloffRadius = falloffRadius;
+ fireChangedListeners();
+ }
+
+ /**
+ * Spotlights shine light in a cone, this value determines the size of the inner part of the cone.
+ * The intensity is interpolated between the inner and outer cone angles - meaning if they are the
+ * same than the cone is perfectly sharp. Generally you will want the inner cone to be smaller
+ * than the outer cone to avoid aliasing.
+ *
+ * @param coneInner inner cone angle in radians, default 0.5
+ */
+ public void setInnerConeAngle(float coneInner) {
+ this.spotlightConeInner = coneInner;
+ fireChangedListeners();
+ }
+
+ /**
+ * Spotlights shine light in a cone, this value determines the size of the outer part of the cone.
+ * The intensity is interpolated between the inner and outer cone angles - meaning if they are the
+ * same than the cone is perfectly sharp. Generally you will want the inner cone to be smaller
+ * than the outer cone to avoid aliasing.
+ *
+ * @param coneOuter outer cone angle in radians, default is 0.6
+ */
+ public void setOuterConeAngle(float coneOuter) {
+ this.spotlightConeOuter = coneOuter;
+ fireChangedListeners();
+ }
+
+ /** Get the light {@link Type}. */
+ public Type getType() {
+ return this.type;
+ }
+
+ /** Returns true if the light has shadow casting enabled. */
+ public boolean isShadowCastingEnabled() {
+ return this.enableShadows;
+ }
+
+ /** @hide This is no longer a user facing API. */
+ public Vector3 getLocalPosition() {
+ return new Vector3(this.position);
+ }
+
+ /** @hide This is no longer a user facing API. */
+ public Vector3 getLocalDirection() {
+ return new Vector3(this.direction);
+ }
+
+ /** Get the RGB {@link Color} of the light. */
+ public Color getColor() {
+ return new Color(this.color);
+ }
+
+ /** Get the intensity of the light. */
+ public float getIntensity() {
+ return this.intensity;
+ }
+
+ /** Get the falloff radius of the light. */
+ public float getFalloffRadius() {
+ return this.falloffRadius;
+ }
+
+ /** Get the inner cone angle for spotlights. */
+ public float getInnerConeAngle() {
+ return this.spotlightConeInner;
+ }
+
+ /** Get the outer cone angle for spotlights. */
+ public float getOuterConeAngle() {
+ return this.spotlightConeOuter;
+ }
+
+ /** @hide this functionality is not part of the end-user API */
+ public LightInstance createInstance(TransformProvider transformProvider) {
+ LightInstance instance = new LightInstance(this, transformProvider);
+ if (instance == null) {
+ throw new AssertionError("Failed to create light instance, result is null.");
+ }
+ return instance;
+ }
+
+ /** Factory class for {@link Light} */
+ public static final class Builder {
+ // LINT.IfChange
+ private static final float DEFAULT_DIRECTIONAL_INTENSITY = 420.0f;
+ // LINT.ThenChange(//depot/google3/third_party/arcore/ar/sceneform/viewer/viewer.cc)
+
+ private final Type type;
+
+ private boolean enableShadows = false;
+ private Vector3 position = new Vector3(0.0f, 0.0f, 0.0f);
+ private Vector3 direction = new Vector3(0.0f, 0.0f, -1.0f);
+ private Color color = new Color(1.0f, 1.0f, 1.0f);
+ private float intensity = 2500.0f;
+ private float falloffRadius = 10.0f;
+ private float spotlightConeInner = 0.5f;
+ private float spotlightConeOuter = 0.6f;
+
+ /** Constructor for building. */
+ private Builder(Type type) {
+ this.type = type;
+ // Directional lights should have a different default intensity
+ if (type == Light.Type.DIRECTIONAL) {
+ intensity = DEFAULT_DIRECTIONAL_INTENSITY;
+ }
+ }
+
+ /**
+ * Determines whether the light casts shadows, or whether synthetic objects can block the light.
+ *
+ * @param enableShadows true to enable to shadows, false to disable; default is false.
+ */
+ public Builder setShadowCastingEnabled(boolean enableShadows) {
+ this.enableShadows = enableShadows;
+ return this;
+ }
+
+ /**
+ * Sets the "RGB" color of the light. Note that intensity if is a separate parameter, so you
+ * should set the pure color (i.e. each channel is in the [0,1] range). However setting values
+ * outside that range is valid.
+ *
+ * @param color "RGB" color, default is (1, 1, 1)
+ */
+ public Builder setColor(Color color) {
+ this.color = color;
+ return this;
+ }
+
+ /**
+ * Sets the "RGB" color of the light based on the desired "color temperature."
+ *
+ * @param temperature color temperature in Kelvin on a scale from 1,000 to 10,000K. Typical
+ * commercial and residential lighting falls somewhere in the 2000K to 6500K range.
+ */
+ public Builder setColorTemperature(float temperature) {
+ final float[] rgbColor = Colors.cct(temperature);
+ setColor(new Color(rgbColor[0], rgbColor[1], rgbColor[2]));
+ return this;
+ }
+
+ /**
+ * Sets the light intensity which determines how bright the light is in Lux (lx) or Lumens (lm)
+ * (depending on the light type). Larger values produce brighter lights and near zero values
+ * generate very little light. A household light bulb will generally have an intensity between
+ * 800 - 2500 lm whereas sunlight will be around 120,000 lx. There is no absolute upper bound
+ * but values larger than sunlight (120,000 lx) are generally not needed.
+ *
+ * @param intensity the intensity of the light, values greater than one are valid. The intensity
+ * will be clamped and cannot be zero or negative. For directional lights the default is 420
+ * lx. For other other lights the default is 2500 lm.
+ */
+ public Builder setIntensity(float intensity) {
+ this.intensity = intensity;
+ return this;
+ }
+
+ /**
+ * Sets the range that the light intensity falls off to zero. This has no affect on infinite
+ * light types - the Directional types.
+ *
+ * @param falloffRadius the light radius in world units, default is 10.0f.
+ */
+ public Builder setFalloffRadius(float falloffRadius) {
+ this.falloffRadius = falloffRadius;
+ return this;
+ }
+
+ /**
+ * Spotlights shine light in a cone, this value determines the size of the inner part of the
+ * cone. The intensity is interpolated between the inner and outer cone angles - meaning if they
+ * are the same than the cone is perfectly sharp. Generally you will want the inner cone to be
+ * smaller than the outer cone to avoid aliasing.
+ *
+ * @param coneInner inner cone angle in radians, default is 0.5
+ */
+ public Builder setInnerConeAngle(float coneInner) {
+ this.spotlightConeInner = coneInner;
+ return this;
+ }
+
+ /**
+ * Spotlights shine light in a cone, this value determines the size of the outer part of the
+ * cone. The intensity is interpolated between the inner and outer cone angles - meaning if they
+ * are the same than the cone is perfectly sharp. Generally you will want the inner cone to be
+ * smaller than the outer cone to avoid aliasing.
+ *
+ * @param coneOuter outer cone angle in radians, default is 0.6
+ */
+ public Builder setOuterConeAngle(float coneOuter) {
+ this.spotlightConeOuter = coneOuter;
+ return this;
+ }
+
+ /** Creates a new {@link Light} based on the parameters set previously */
+ public Light build() {
+ Light light = new Light(this);
+ if (light == null) {
+ throw new AssertionError("Allocating a new light failed.");
+ }
+ return light;
+ }
+ }
+
+ /**
+ * package-private function to add listeners so that light instances can be updated when light
+ * parameters change.
+ */
+ void addChangedListener(LightChangedListener listener) {
+ changedListeners.add(listener);
+ }
+
+ /** package-private function to remove change listeners. */
+ void removeChangedListener(LightChangedListener listener) {
+ changedListeners.remove(listener);
+ }
+
+ private Light(Builder builder) {
+ this.type = builder.type;
+ this.enableShadows = builder.enableShadows;
+ this.position = builder.position;
+ this.direction = builder.direction;
+ this.color = builder.color;
+ this.intensity = builder.intensity;
+ this.falloffRadius = builder.falloffRadius;
+ this.spotlightConeInner = builder.spotlightConeInner;
+ this.spotlightConeOuter = builder.spotlightConeOuter;
+ }
+
+ private void fireChangedListeners() {
+ for (LightChangedListener listener : changedListeners) {
+ listener.onChange();
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LightInstance.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LightInstance.java
new file mode 100644
index 0000000..0edeaa5
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LightInstance.java
@@ -0,0 +1,231 @@
+package com.google.ar.sceneform.rendering;
+
+import android.util.Log;
+import androidx.annotation.Nullable;
+import com.google.android.filament.Entity;
+import com.google.android.filament.EntityManager;
+import com.google.android.filament.LightManager;
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+
+/**
+ * Wraps a Filament Light.
+ *
+ * @hide This class is not part of the user facing API.
+ */
+public class LightInstance {
+ private static final String TAG = LightInstance.class.getSimpleName();
+
+ private class LightInstanceChangeListener implements Light.LightChangedListener {
+ @Override
+ public void onChange() {
+ dirty = true;
+ }
+ }
+
+ private @Entity final int entity;
+ private final Light light;
+ @Nullable private Renderer renderer;
+
+ @Nullable private TransformProvider transformProvider = null;
+
+ private Vector3 localPosition;
+ private Vector3 localDirection;
+ private boolean dirty;
+
+ private LightInstanceChangeListener changeListener = new LightInstanceChangeListener();
+
+ LightInstance(Light light, TransformProvider transformProvider) {
+ this.light = light;
+ this.transformProvider = transformProvider;
+ this.localPosition = light.getLocalPosition();
+ this.localDirection = light.getLocalDirection();
+ this.dirty = false;
+
+ // Add a listener so the light instance knows when the light changes.
+ light.addChangedListener(changeListener);
+
+ entity = EntityManager.get().create();
+ IEngine engine = EngineInstance.getEngine();
+
+ // Filament will crash if you call functions on the builder that are not appropriate for the
+ // light type.
+ if (light.getType() == Light.Type.POINT) {
+ new LightManager.Builder(LightManager.Type.POINT)
+ .position(
+ light.getLocalPosition().x, light.getLocalPosition().y, light.getLocalPosition().z)
+ .color(light.getColor().r, light.getColor().g, light.getColor().b)
+ .intensity(light.getIntensity())
+ .falloff(light.getFalloffRadius())
+ .castShadows(light.isShadowCastingEnabled())
+ .build(engine.getFilamentEngine(), entity);
+ } else if (light.getType() == Light.Type.DIRECTIONAL) {
+ new LightManager.Builder(LightManager.Type.DIRECTIONAL)
+ .direction(
+ light.getLocalDirection().x, light.getLocalDirection().y, light.getLocalDirection().z)
+ .color(light.getColor().r, light.getColor().g, light.getColor().b)
+ .intensity(light.getIntensity())
+ .castShadows(light.isShadowCastingEnabled())
+ .build(engine.getFilamentEngine(), entity);
+ } else if (light.getType() == Light.Type.SPOTLIGHT) {
+ new LightManager.Builder(LightManager.Type.SPOT)
+ .position(
+ light.getLocalPosition().x, light.getLocalPosition().y, light.getLocalPosition().z)
+ .direction(
+ light.getLocalDirection().x, light.getLocalDirection().y, light.getLocalDirection().z)
+ .color(light.getColor().r, light.getColor().g, light.getColor().b)
+ .intensity(light.getIntensity())
+ .spotLightCone(
+ Math.min(light.getInnerConeAngle(), light.getOuterConeAngle()),
+ light.getOuterConeAngle())
+ .castShadows(light.isShadowCastingEnabled())
+ .build(engine.getFilamentEngine(), entity);
+ } else if (light.getType() == Light.Type.FOCUSED_SPOTLIGHT) {
+ new LightManager.Builder(LightManager.Type.FOCUSED_SPOT)
+ .position(
+ light.getLocalPosition().x, light.getLocalPosition().y, light.getLocalPosition().z)
+ .direction(
+ light.getLocalDirection().x, light.getLocalDirection().y, light.getLocalDirection().z)
+ .color(light.getColor().r, light.getColor().g, light.getColor().b)
+ .intensity(light.getIntensity())
+ .spotLightCone(
+ Math.min(light.getInnerConeAngle(), light.getOuterConeAngle()),
+ light.getOuterConeAngle())
+ .castShadows(light.isShadowCastingEnabled())
+ .build(engine.getFilamentEngine(), entity);
+ } else {
+ throw new UnsupportedOperationException("Unsupported light type.");
+ }
+ }
+
+ public void updateTransform() {
+ // Update the light instance based on changes to the source light.
+ updateProperties();
+
+ // Handle lights that do not have transform providers such as default global sunlight.
+ if (transformProvider == null) {
+ return;
+ }
+
+ IEngine engine = EngineInstance.getEngine();
+ LightManager lightManager = engine.getLightManager();
+
+ final int instance = lightManager.getInstance(entity);
+ final Matrix transform = transformProvider.getWorldModelMatrix();
+
+ if (lightTypeRequiresPosition(light.getType())) {
+ final Vector3 position = transform.transformPoint(localPosition);
+ lightManager.setPosition(instance, position.x, position.y, position.z);
+ }
+ if (lightTypeRequiresDirection(light.getType())) {
+ final Vector3 direction = transform.transformDirection(localDirection);
+ lightManager.setDirection(instance, direction.x, direction.y, direction.z);
+ }
+ }
+
+ public void attachToRenderer(Renderer renderer) {
+ renderer.addLight(this);
+ this.renderer = renderer;
+ }
+
+ public void detachFromRenderer() {
+ if (renderer != null) {
+ renderer.removeLight(this);
+ }
+ }
+
+ public Light getLight() {
+ return light;
+ }
+
+ @Entity
+ int getEntity() {
+ return entity;
+ }
+
+ public void dispose() {
+ AndroidPreconditions.checkUiThread();
+
+ // Remove the changed listener from the light so the light instance's memory can be freed.
+ if (light != null) {
+ light.removeChangedListener(changeListener);
+ changeListener = null;
+ }
+
+ IEngine engine = EngineInstance.getEngine();
+ if (engine != null && engine.isValid()) {
+ LightManager lightManager = engine.getLightManager();
+ lightManager.destroy(entity);
+
+ EntityManager entityManager = EntityManager.get();
+ entityManager.destroy(entity);
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ ThreadPools.getMainExecutor().execute(() -> dispose());
+ } catch (Exception e) {
+ Log.e(TAG, "Error while Finalizing Light Instance.", e);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /*
+ * Copy updated light properites from the light data
+ * This just updates a light rather than creating a new one.
+ */
+ private void updateProperties() {
+ // Only update the properties if the light is marked as dirty.
+ if (!dirty) {
+ return;
+ }
+ dirty = false;
+
+ IEngine engine = EngineInstance.getEngine();
+ LightManager lightManager = engine.getLightManager();
+ final int instance = lightManager.getInstance(entity);
+
+ localPosition = light.getLocalPosition();
+ localDirection = light.getLocalDirection();
+
+ // Handle lights that are not attached to nodes, treat local direction/position as world space.
+ if (transformProvider == null) {
+ if (lightTypeRequiresPosition(light.getType())) {
+ lightManager.setPosition(instance, localPosition.x, localPosition.y, localPosition.z);
+ }
+ if (lightTypeRequiresDirection(light.getType())) {
+ lightManager.setDirection(instance, localDirection.x, localDirection.y, localDirection.z);
+ }
+ }
+
+ lightManager.setColor(instance, light.getColor().r, light.getColor().g, light.getColor().b);
+ lightManager.setIntensity(instance, light.getIntensity());
+ if (light.getType() == Light.Type.POINT) {
+ lightManager.setFalloff(instance, light.getFalloffRadius());
+ } else if (light.getType() == Light.Type.SPOTLIGHT
+ || light.getType() == Light.Type.FOCUSED_SPOTLIGHT) {
+ lightManager.setSpotLightCone(
+ instance,
+ Math.min(light.getInnerConeAngle(), light.getOuterConeAngle()),
+ light.getOuterConeAngle());
+ }
+ }
+
+ private static boolean lightTypeRequiresPosition(Light.Type type) {
+ return type == Light.Type.POINT
+ || type == Light.Type.SPOTLIGHT
+ || type == Light.Type.FOCUSED_SPOTLIGHT;
+ }
+
+ private static boolean lightTypeRequiresDirection(Light.Type type) {
+ return type == Light.Type.SPOTLIGHT
+ || type == Light.Type.FOCUSED_SPOTLIGHT
+ || type == Light.Type.DIRECTIONAL;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LightProbe.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LightProbe.java
new file mode 100644
index 0000000..6af5170
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LightProbe.java
@@ -0,0 +1,709 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.Image;
+import android.net.Uri;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import com.google.android.filament.IndirectLight;
+import com.google.android.filament.Texture;
+
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Quaternion;
+import com.google.ar.sceneform.rendering.SceneformBundle.VersionException;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.ChangeId;
+import com.google.ar.sceneform.utilities.EnvironmentalHdrParameters;
+import com.google.ar.sceneform.utilities.LoadHelper;
+import com.google.ar.sceneform.utilities.Preconditions;
+import com.google.ar.sceneform.utilities.SceneformBufferUtils;
+import com.google.ar.schemas.lull.Vec3;
+import com.google.ar.schemas.sceneform.LightingCubeDef;
+import com.google.ar.schemas.sceneform.LightingCubeFaceDef;
+import com.google.ar.schemas.sceneform.LightingCubeFaceType;
+import com.google.ar.schemas.sceneform.LightingDef;
+import com.google.ar.schemas.sceneform.SceneformBundleDef;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+/**
+ * Loads "light probe" data needed for Image Based Lighting. This includes a cubemap with mip maps
+ * generated to match the lighting model used by Sceneform and Spherical Harmonics coefficients for
+ * diffuse image based lighting.
+ *
+ * @hide for 1.0 as we don't yet have tools support
+ */
+public class LightProbe {
+ private static final String TAG = LightProbe.class.getSimpleName();
+ private static final int CUBEMAP_MIN_WIDTH = 4;
+ private static final int CUBEMAP_FACE_COUNT = 6;
+ private static final int RGBM_BYTES_PER_PIXEL = 4;
+ private static final int FLOATS_PER_VECTOR = 3;
+ private static final int SH_ORDER = 3;
+ private static final int BYTES_PER_FLOAT16 = 2;
+ private static final int RGBA_BYTES_PER_PIXEL = 8;
+ private static final int RGB_BYTES_PER_PIXEL = 6;
+ private static final int RGB_CHANNEL_COUNT = 3;
+ // The Light estimate scale and offset allow the final change in intensity to be controlled to
+ // avoid over darkening or changes that are too drastic: appliedEstimate = estimate*scale + offset
+ private static final float LIGHT_ESTIMATE_SCALE = 1.8f;
+ private static final float LIGHT_ESTIMATE_OFFSET = 0.0f;
+ // The number of required SH vectors = SH_ORDER^2
+ private static final int SH_VECTORS_FOR_THIRD_ORDER = SH_ORDER * SH_ORDER;
+ // Filament expects the face order to be "px", "nx", "py", "ny", "pz", "nz"
+ private static final int[] FACE_TO_FILAMENT_MAPPING = {
+ LightingCubeFaceType.px,
+ LightingCubeFaceType.nx,
+ LightingCubeFaceType.py,
+ LightingCubeFaceType.ny,
+ LightingCubeFaceType.pz,
+ LightingCubeFaceType.nz
+ };
+ private static final int EXPECTED_SPHERICAL_HARMONICS_LENGTH = 27;
+
+ /**
+ * Convert Environmental HDR's spherical harmonics to Filament spherical harmonics.
+ *
+ * This conversion is calculated to include the following:
+ *
+ *
+ * - pre-scaling by SH basis normalization factor [shader optimization]
+ *
- sqrt(2) factor coming from keeping only the real part of the basis [shader optimization]
+ *
- 1/pi factor for the diffuse lambert BRDF [shader optimization]
+ *
- |dot(n,l)| spherical harmonics [irradiance]
+ *
- scaling for convolution of SH function by radially symmetrical SH function [irradiance]
+ *
+ *
+ * ENVIRONMENTAL_HDR_TO_FILAMENT_SH_INDEX_MAP must be applied change ordering of coeffients
+ * from Environmental HDR to filament.
+ */
+ private static final float[] ENVIRONMENTAL_HDR_TO_FILAMENT_SH_COEFFIECIENTS = {
+ 0.282095f,
+ -0.325735f,
+ 0.325735f,
+ -0.325735f,
+ 0.273137f,
+ -0.273137f,
+ 0.078848f, // notice index 6 & 7 swapped
+ -0.273137f, // notice index 6 & 7 swapped
+ 0.136569f
+ };
+
+ // SH coefficients are not in the same order in Filament and Environmental HDR.
+ // SH coefficients at indices 6 and 7 are swapped between the two implementations.
+ private static final int[] ENVIRONMENTAL_HDR_TO_FILAMENT_SH_INDEX_MAP = {
+ 0, 1, 2, 3, 4, 5, 7, 6, 8
+ };
+
+ private ByteBuffer cubemapBuffer = ByteBuffer.allocate(10000);
+ @Nullable private Texture reflectCubemap = null;
+ private final Color colorCorrection = new Color(1f, 1f, 1f);
+ private final Color ambientColor = new Color();
+ private float[] irradianceData;
+ @Nullable private String name = null;
+
+ private ChangeId changeId = new ChangeId();
+
+ private float intensity;
+ private float lightEstimate = 1.0f;
+ @Nullable private Quaternion rotation;
+
+ /** Constructs a default LightProbe, if nothing else is set */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Set the overall intensity of the indirect light.
+ *
+ * @param intensity the intensity of indirect lighting, the default is 220.0
+ */
+ public void setIntensity(float intensity) {
+ this.intensity = intensity;
+ }
+
+ /** Get the overall intensity of the indirect light. */
+ public float getIntensity() {
+ return intensity;
+ }
+
+ /**
+ * Sets the rotation of the indirect light.
+ *
+ * @param rotation the rotation of the indirect light, identity when null
+ */
+ public void setRotation(@Nullable Quaternion rotation) {
+ this.rotation = rotation;
+ }
+
+ /** Gets the rotation of the indirect light, identity if null. */
+ @Nullable
+ public Quaternion getRotation() {
+ return rotation;
+ }
+
+ int getId() {
+ return changeId.get();
+ }
+
+ /** Returns true if the LightProbe is ready to be used for rendering. */
+ public boolean isReady() {
+ return !changeId.isEmpty();
+ }
+
+ /** @hide */
+ @Nullable
+ com.google.android.filament.IndirectLight buildIndirectLight() {
+ Preconditions.checkNotNull(irradianceData, "\"irradianceData\" was null.");
+ Preconditions.checkState(
+ irradianceData.length >= FLOATS_PER_VECTOR,
+ "\"irradianceData\" does not have enough components to store a vector");
+
+ if (reflectCubemap == null) {
+ throw new IllegalStateException("reflectCubemap is null.");
+ }
+
+ // Modulates ambient color with modulation factor. irradianceData must have at least one vector
+ // of three floats.
+ irradianceData[0] = ambientColor.r * colorCorrection.r;
+ irradianceData[1] = ambientColor.g * colorCorrection.g;
+ irradianceData[2] = ambientColor.b * colorCorrection.b;
+
+ IndirectLight indirectLight =
+ new IndirectLight.Builder()
+ .reflections(reflectCubemap)
+ .irradiance(SH_ORDER, irradianceData)
+ .intensity(intensity * lightEstimate)
+ .build(EngineInstance.getEngine().getFilamentEngine());
+
+ // There is a bug in filament where setting the rotation doesn't work if it is done using
+ // the builder. It must be done on the actual indirect light object.
+ if (rotation != null) {
+ indirectLight.setRotation(quaternionToRotationMatrix(rotation));
+ }
+
+ if (indirectLight == null) {
+ throw new IllegalStateException("Light Probe is invalid.");
+ }
+ return indirectLight;
+ }
+
+ private LightProbe(Builder builder) {
+ intensity = builder.intensity;
+ rotation = builder.rotation;
+ name = builder.name;
+ }
+
+ private void buildFilamentResource(LightingDef lightingDef) {
+ dispose();
+ changeId.update();
+
+ // For some reason the static analysis cannot see the check for null above for LightingDef,
+ // so we check again here...
+ if (lightingDef == null) {
+ throw new IllegalStateException(
+ "buildFilamentResource called but no resource is available to load.");
+ }
+
+ // Build the resources here.
+ final Texture cubemap = loadReflectCubemapFromLightingDef(lightingDef);
+ if (cubemap == null) {
+ throw new IllegalStateException("Load reflection cubemap failed.");
+ }
+ setCubeMapFromTexture(cubemap);
+
+ final int shVectorCount = lightingDef.shCoefficientsLength();
+ if (shVectorCount < SH_VECTORS_FOR_THIRD_ORDER) {
+ throw new IllegalStateException("Too few SH vectors for the current Order (3).");
+ }
+
+ final int requiredFloatCount = shVectorCount * FLOATS_PER_VECTOR;
+ if (irradianceData == null || irradianceData.length != requiredFloatCount) {
+ irradianceData = new float[requiredFloatCount];
+ }
+
+ for (int v = 0; v < shVectorCount; ++v) {
+ final Vec3 shVector = lightingDef.shCoefficients(v);
+ // filament SH coefficient have changed for 1.6, since we use hardcoded coefficients for now
+ // we have to scale them to the new format.
+
+ // LINT.IfChange
+ irradianceData[v * 3 + 0] = shVector.x() / (float) Math.PI;
+ irradianceData[v * 3 + 1] = shVector.y() / (float) Math.PI;
+ irradianceData[v * 3 + 2] = shVector.z() / (float) Math.PI;
+ // LINT.ThenChange(//depot/google3/third_party/arcore/ar/imp/core/lighting/lighting_data.cc)
+ }
+
+ // Gets ambient color of irradiance data as the first sh coefficient.
+ ambientColor.set(irradianceData[0], irradianceData[1], irradianceData[2]);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ ThreadPools.getMainExecutor().execute(() -> dispose());
+ } catch (Exception e) {
+ Log.e(TAG, "Error while Finalizing Light Probe.", e);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /** @hide */
+ @SuppressWarnings("nullness")
+ public void dispose() {
+ AndroidPreconditions.checkUiThread();
+
+ setCubeMapFromTexture(null);
+
+ changeId = new ChangeId();
+ }
+
+ private void setCubeMapFromTexture(com.google.android.filament.Texture nextCubemap) {
+ com.google.android.filament.Texture prevTexture = reflectCubemap;
+ IEngine engine = EngineInstance.getEngine();
+ if (prevTexture != null && engine != null && engine.isValid()) {
+ engine.destroyTexture(prevTexture);
+ }
+ reflectCubemap = nextCubemap;
+ }
+
+ /**
+ * Updates spherical harmonics with values not premultiplied by the SH basis.
+ *
+ * @hide intended for use by other Sceneform packages which update Hdr lighting every frame.
+ */
+
+ public void setEnvironmentalHdrSphericalHarmonics(
+ float[] sphericalHarmonics,
+ float exposure,
+ EnvironmentalHdrParameters environmentalHdrParameters) {
+ float scaleFactor =
+ environmentalHdrParameters.getAmbientShScaleForFilament()
+ / (exposure * environmentalHdrParameters.getReflectionScaleForFilament());
+ if (sphericalHarmonics.length != EXPECTED_SPHERICAL_HARMONICS_LENGTH) {
+ throw new RuntimeException(
+ "Expected " + EXPECTED_SPHERICAL_HARMONICS_LENGTH + " spherical Harmonics coefficients");
+ }
+
+ if (irradianceData == null || irradianceData.length != sphericalHarmonics.length) {
+ irradianceData = new float[EXPECTED_SPHERICAL_HARMONICS_LENGTH];
+ }
+
+ for (int srcIndex = 0; srcIndex < 9; ++srcIndex) {
+ int destIndex = ENVIRONMENTAL_HDR_TO_FILAMENT_SH_INDEX_MAP[srcIndex];
+ irradianceData[destIndex * 3] =
+ sphericalHarmonics[srcIndex * 3]
+ * ENVIRONMENTAL_HDR_TO_FILAMENT_SH_COEFFIECIENTS[destIndex]
+ * scaleFactor;
+ irradianceData[destIndex * 3 + 1] =
+ sphericalHarmonics[srcIndex * 3 + 1]
+ * ENVIRONMENTAL_HDR_TO_FILAMENT_SH_COEFFIECIENTS[destIndex]
+ * scaleFactor;
+
+ irradianceData[destIndex * 3 + 2] =
+ sphericalHarmonics[srcIndex * 3 + 2]
+ * ENVIRONMENTAL_HDR_TO_FILAMENT_SH_COEFFIECIENTS[destIndex]
+ * scaleFactor;
+ }
+ ambientColor.set(irradianceData[0], irradianceData[1], irradianceData[2]);
+ this.colorCorrection.set(new Color(1, 1, 1));
+ this.lightEstimate = environmentalHdrParameters.getReflectionScaleForFilament();
+ this.intensity = 1.0f;
+ }
+
+ /**
+ * Modify light intensity using ArCore light estimation. ArCore light estimation is not compatible
+ * with Environmental HDR, only one may be used.
+ *
+ * @hide
+ */
+ public void setLightEstimate(Color colorCorrection, float estimate) {
+ // Scale and bias the estimate to avoid over darkening.
+ lightEstimate = Math.min(estimate * LIGHT_ESTIMATE_SCALE + LIGHT_ESTIMATE_OFFSET, 1.0f);
+ this.colorCorrection.set(colorCorrection);
+ }
+
+ /**
+ * Constructs a {@link LightProbe} when the data is unavailable, and must be requested
+ * asynchronously
+ */
+ @SuppressWarnings("AndroidApiChecker") // java.util.concurrent.CompletableFuture
+ private CompletableFuture loadInBackground(
+ Callable inputStreamCreator) {
+ return CompletableFuture.supplyAsync(
+ () -> {
+ if (inputStreamCreator == null) {
+ throw new IllegalArgumentException("Invalid source.");
+ }
+
+ @Nullable ByteBuffer assetData = null;
+
+ // Open and read the texture file.
+ try (InputStream inputStream = inputStreamCreator.call()) {
+ assetData = SceneformBufferUtils.readStream(inputStream);
+ } catch (Exception e) {
+ throw new CompletionException(e);
+ }
+
+ if (assetData == null) {
+ throw new AssertionError(
+ "The Sceneform bundle containing the Light Probe could not be loaded.");
+ }
+
+ SceneformBundleDef rcb;
+ try {
+ rcb = SceneformBundle.tryLoadSceneformBundle(assetData);
+ } catch (VersionException e) {
+ throw new CompletionException(e);
+ }
+
+ if (rcb == null) {
+ throw new AssertionError(
+ "The Sceneform bundle containing the Light Probe could not be loaded.");
+ }
+
+ final int lightingDefsLength = rcb.lightingDefsLength();
+ if (lightingDefsLength < 1) {
+ throw new IllegalStateException("Content does not contain any Light Probe data.");
+ }
+
+ // If the name is non-null, look for the correct Light Probe to use.
+ // If the name is null then the first Light Probe is used.
+ int lightProbeIndex = -1;
+ if (name != null) {
+ for (int i = 0; i < lightingDefsLength; ++i) {
+ LightingDef lightingDef = rcb.lightingDefs(i);
+ if (lightingDef.name().equals(name)) {
+ lightProbeIndex = i;
+ break;
+ }
+ }
+
+ if (lightProbeIndex < 0) {
+ throw new IllegalArgumentException(
+ "Light Probe asset \"" + name + "\" not found in bundle.");
+ }
+ } else {
+ lightProbeIndex = 0;
+ }
+
+ LightingDef lightingDef = rcb.lightingDefs(lightProbeIndex);
+ if (lightingDef == null) {
+ throw new IllegalStateException("LightingDef is invalid.");
+ }
+
+ return lightingDef;
+ },
+ ThreadPools.getThreadPoolExecutor());
+ }
+
+ /** Factory class for {@link LightProbe} */
+ @SuppressWarnings("AndroidApiChecker") // java.util.concurrent.CompletableFuture
+ public static final class Builder {
+ /** The {@link LightProbe} will be constructed from the contents of this callable */
+ @Nullable private Callable inputStreamCreator = null;
+
+ /** intensity of the indirect lighting */
+ private float intensity = 220.0f;
+
+ @Nullable private Quaternion rotation;
+
+ /**
+ * Name of the Light Probe to load if the file contains more than one. If no name is specified
+ * than the first Light Probe found will be used.
+ */
+ @Nullable private String name = null;
+
+ /** Constructor for asynchronous building. */
+ private Builder() {}
+
+ /**
+ * Set the intensity of the indirect lighting.
+ *
+ * @param intensity intensity of the indirect lighting, the default is 220.
+ */
+ public Builder setIntensity(float intensity) {
+ this.intensity = intensity;
+ return this;
+ }
+
+ /**
+ * Sets the rotation of the indirect light.
+ *
+ * @param rotation the rotation of the indirect light, identity when null
+ */
+ public Builder setRotation(@Nullable Quaternion rotation) {
+ this.rotation = rotation;
+ return this;
+ }
+
+ /**
+ * Set the name of the Light Probe to load if the binary bundle file contains more than one.
+ *
+ * @param name the name of the Light Probe to load.
+ */
+ public Builder setAssetName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Allows a {@link LightProbe} to be constructed from {@link Uri}. Construction will be
+ * asynchronous.
+ *
+ * @param context a context used for loading the resource
+ * @param sourceUri a remote Uri or android resource Uri.
+ * @hide Hide until we have a documented way to build custom light probes.
+ */
+ public Builder setSource(Context context, Uri sourceUri) {
+ Preconditions.checkNotNull(sourceUri, "Parameter \"sourceUri\" was null.");
+
+ setSource(LoadHelper.fromUri(context, sourceUri));
+ return this;
+ }
+
+ /**
+ * Allows a {@link LightProbe} to be constructed from resource. Construction will be
+ * asynchronous.
+ *
+ * @param context a context used for loading the resource
+ * @param resource an android resource with raw type.
+ */
+ public Builder setSource(Context context, int resource) {
+ setSource(LoadHelper.fromResource(context, resource));
+ return this;
+ }
+
+ /**
+ * Allows a {@link LightProbe} to be constructed via callable function.
+ *
+ * @hide Hide until we have a documented way to build custom light probes.
+ */
+ public Builder setSource(Callable inputStreamCreator) {
+ Preconditions.checkNotNull(
+ inputStreamCreator, "Parameter \"sourceInputStreamCallable\" was null.");
+
+ this.inputStreamCreator = inputStreamCreator;
+ return this;
+ }
+
+ /** Creates a new {@link LightProbe} based on the parameters set previously */
+ @SuppressWarnings("FutureReturnValueIgnored") // CompletableFuture
+ public CompletableFuture build() {
+ // At this point sourceInputStreamCallable should never be null.
+ if (inputStreamCreator == null) {
+ throw new IllegalStateException("Light Probe source is NULL, this should never happen.");
+ }
+
+ @Nullable LightProbe lightProbe = new LightProbe(this);
+ CompletableFuture result =
+ lightProbe
+ .loadInBackground(inputStreamCreator)
+ .thenApplyAsync(
+ lightingDef -> {
+ // Call to buildFilamentResource on the Filament thread
+ lightProbe.buildFilamentResource(lightingDef);
+ return lightProbe;
+ },
+ ThreadPools.getMainExecutor());
+
+ if (result == null) {
+ throw new IllegalStateException("CompletableFuture result is null.");
+ }
+
+ return FutureHelper.logOnException(
+ TAG, result, "Unable to load LightProbe: name='" + name + "'");
+ }
+ }
+
+ public void setCubeMap(Image[] cubemapImageArray) {
+ // TODO: Update once Filament updates past v1.3.0.
+ if (cubemapImageArray.length != CUBEMAP_FACE_COUNT) {
+ throw new IllegalArgumentException(
+ "Unexpected cubemap array length: " + cubemapImageArray.length);
+ }
+
+ int width = cubemapImageArray[0].getWidth();
+ int height = cubemapImageArray[0].getHeight();
+ int bufferCapacity =
+ width * height * CUBEMAP_FACE_COUNT * RGB_CHANNEL_COUNT * BYTES_PER_FLOAT16;
+ if (cubemapBuffer.capacity() < bufferCapacity) {
+ cubemapBuffer = ByteBuffer.allocate(bufferCapacity);
+ } else {
+ cubemapBuffer.clear();
+ }
+
+ int[] faceOffsets = new int[CUBEMAP_FACE_COUNT];
+ for (int i = 0; i < CUBEMAP_FACE_COUNT; i++) {
+ faceOffsets[i] = cubemapBuffer.position();
+ Image.Plane[] planes = cubemapImageArray[i].getPlanes();
+ if (planes.length != 1) {
+ throw new IllegalArgumentException(
+ "Unexpected number of Planes in cubemap Image array: " + planes.length);
+ }
+ Image.Plane currentPlane = planes[0];
+ if (currentPlane.getPixelStride() != RGBA_BYTES_PER_PIXEL) {
+ throw new IllegalArgumentException(
+ "Unexpected pixel stride in cubemap data: expected "
+ + RGBA_BYTES_PER_PIXEL
+ + ", got "
+ + currentPlane.getPixelStride());
+ }
+ if (currentPlane.getRowStride() != width * RGBA_BYTES_PER_PIXEL) {
+ throw new IllegalArgumentException(
+ "Unexpected row stride in cubemap data: expected "
+ + (width * RGBA_BYTES_PER_PIXEL)
+ + ", got "
+ + currentPlane.getRowStride());
+ }
+ ByteBuffer rgbaBuffer = currentPlane.getBuffer();
+ while (rgbaBuffer.hasRemaining()) {
+ for (int byt = 0; byt < RGBA_BYTES_PER_PIXEL; byt++) {
+ byte b = rgbaBuffer.get();
+ if (byt < RGB_BYTES_PER_PIXEL) {
+ cubemapBuffer.put(b);
+ }
+ }
+ }
+ }
+ cubemapBuffer.flip();
+
+ IEngine engine = EngineInstance.getEngine();
+ int levels = (int) (1 + Math.log(width) / Math.log(2.0));
+ Texture cubemapTexture =
+ new com.google.android.filament.Texture.Builder()
+ .width(width)
+ .height(height)
+ .levels(levels)
+ .sampler(com.google.android.filament.Texture.Sampler.SAMPLER_CUBEMAP)
+ .format(com.google.android.filament.Texture.InternalFormat.R11F_G11F_B10F)
+ .build(engine.getFilamentEngine());
+ com.google.android.filament.Texture.PixelBufferDescriptor pixelBuf =
+ new com.google.android.filament.Texture.PixelBufferDescriptor(
+ cubemapBuffer,
+ com.google.android.filament.Texture.Format.RGB,
+ com.google.android.filament.Texture.Type.HALF);
+ com.google.android.filament.Texture.PrefilterOptions options =
+ new com.google.android.filament.Texture.PrefilterOptions();
+ options.mirror = false;
+ cubemapTexture.generatePrefilterMipmap(
+ engine.getFilamentEngine(), pixelBuf, faceOffsets, options);
+ setCubeMapFromTexture(cubemapTexture);
+ }
+
+ private static Texture loadReflectCubemapFromLightingDef(LightingDef lightingDef) {
+ Preconditions.checkNotNull(lightingDef, "Parameter \"lightingDef\" was null.");
+
+ IEngine engine = EngineInstance.getEngine();
+
+ final int mipCount = lightingDef.cubeLevelsLength();
+ if (mipCount < 1) {
+ throw new IllegalStateException("Lighting cubemap has no image data.");
+ }
+
+ // Get the size of each face from the first mip map.
+ final LightingCubeDef baseLevel = lightingDef.cubeLevels(0);
+ final LightingCubeFaceDef baseFace = baseLevel.faces(0);
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPremultiplied = false;
+ options.inScaled = false;
+ options.inJustDecodeBounds = true;
+ final ByteBuffer data = baseFace.dataAsByteBuffer();
+
+ final byte[] dataArray = data.array();
+ final int offset = data.arrayOffset() + data.position();
+ final int length = data.limit() - data.position();
+ BitmapFactory.decodeByteArray(dataArray, offset, length, options);
+
+ // Width and Height must be non-zero and equal.
+ int width = options.outWidth;
+ int height = options.outHeight;
+ if (width < CUBEMAP_MIN_WIDTH || height < CUBEMAP_MIN_WIDTH || width != height) {
+ throw new IllegalStateException(
+ "Lighting cubemap has invalid dimensions: " + width + " x " + height);
+ }
+
+ // Create the Filament texture resource.
+ Texture filamentTexture =
+ new com.google.android.filament.Texture.Builder()
+ .width(width)
+ .height(height)
+ .levels(mipCount)
+ .format(com.google.android.filament.Texture.InternalFormat.R11F_G11F_B10F)
+ .sampler(com.google.android.filament.Texture.Sampler.SAMPLER_CUBEMAP)
+ .build(engine.getFilamentEngine());
+
+ // Loop through all of the mip maps and load the image data.
+ int faceSize = width * height * RGBM_BYTES_PER_PIXEL;
+ final int[] faceOffsetsInBytes = new int[CUBEMAP_FACE_COUNT];
+
+ options.inJustDecodeBounds = false;
+ for (int m = 0; m < mipCount; ++m) {
+ // Now load all of the image data into the buffer.
+ final ByteBuffer buffer = ByteBuffer.allocateDirect(faceSize * CUBEMAP_FACE_COUNT);
+ LightingCubeDef level = lightingDef.cubeLevels(m);
+
+ for (int f = 0; f < CUBEMAP_FACE_COUNT; ++f) {
+ final int sourceFaceIndex = FACE_TO_FILAMENT_MAPPING[f];
+ final LightingCubeFaceDef face = level.faces(sourceFaceIndex);
+ faceOffsetsInBytes[f] = faceSize * f;
+
+ final ByteBuffer faceData = face.dataAsByteBuffer();
+ final byte[] faceDataArray = faceData.array();
+ final int faceOffset = faceData.arrayOffset() + faceData.position();
+ final int faceLength = faceData.limit() - faceData.position();
+
+ final Bitmap faceBitmap =
+ BitmapFactory.decodeByteArray(faceDataArray, faceOffset, faceLength, options);
+
+ if (faceBitmap.getWidth() != width || faceBitmap.getHeight() != height) {
+ throw new AssertionError("All cube map textures must have the same size");
+ }
+ faceBitmap.copyPixelsToBuffer(buffer);
+ }
+ buffer.rewind();
+
+ final com.google.android.filament.Texture.PixelBufferDescriptor descriptor =
+ new com.google.android.filament.Texture.PixelBufferDescriptor(
+ buffer,
+ com.google.android.filament.Texture.Format.RGB,
+ com.google.android.filament.Texture.Type.UINT_10F_11F_11F_REV);
+
+ filamentTexture.setImage(engine.getFilamentEngine(), m, descriptor, faceOffsetsInBytes);
+
+ width >>= 1;
+ height >>= 1;
+ faceSize = width * height * RGBM_BYTES_PER_PIXEL;
+ }
+
+ return filamentTexture;
+ }
+
+ private static float[] quaternionToRotationMatrix(Quaternion quaternion) {
+ Matrix matrix = new Matrix();
+ matrix.makeRotation(quaternion);
+
+ // Convert to a 3x3 matrix packed in a float-array.
+ float[] floatArray = new float[9];
+ floatArray[0] = matrix.data[0];
+ floatArray[1] = matrix.data[1];
+ floatArray[2] = matrix.data[2];
+
+ floatArray[3] = matrix.data[4];
+ floatArray[4] = matrix.data[5];
+ floatArray[5] = matrix.data[6];
+
+ floatArray[6] = matrix.data[8];
+ floatArray[7] = matrix.data[9];
+ floatArray[8] = matrix.data[10];
+
+ return floatArray;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LoadGltfListener.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LoadGltfListener.java
new file mode 100644
index 0000000..c04b072
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LoadGltfListener.java
@@ -0,0 +1,29 @@
+package com.google.ar.sceneform.rendering;
+
+/** Interface callbacks for events that occur when loading a gltf file into a renderable. */
+public interface LoadGltfListener {
+ /** Defines the current stage of the load operation, each value supersedes the previous. */
+ public enum GltfLoadStage {
+ LOAD_STAGE_NONE,
+ FETCH_MATERIALS,
+ DOWNLOAD_MODEL,
+ CREATE_LOADER,
+ ADD_MISSING_FILES,
+ FINISHED_READING_FILES,
+ CREATE_RENDERABLE
+ }
+
+ void setLoadingStage(GltfLoadStage stage);
+
+ void reportBytesDownloaded(long bytes);
+
+ void onFinishedFetchingMaterials();
+
+ void onFinishedLoadingModel(long durationMs);
+
+ void onFinishedReadingFiles(long durationMs);
+
+ void setModelSize(float modelSizeMeters);
+
+ void onReadingFilesFailed(Exception exception);
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LoadRenderableFromFilamentGltfTask.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LoadRenderableFromFilamentGltfTask.java
new file mode 100644
index 0000000..7eb6224
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LoadRenderableFromFilamentGltfTask.java
@@ -0,0 +1,102 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import android.net.Uri;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.filament.gltfio.ResourceLoader;
+import com.google.ar.sceneform.utilities.Preconditions;
+import com.google.ar.sceneform.utilities.SceneformBufferUtils;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.function.Function;
+
+/** Task for initializing a renderable with glTF data loaded with gltfio. */
+@SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"}) // CompletableFuture
+public class LoadRenderableFromFilamentGltfTask {
+ private static final String TAG = LoadRenderableFromFilamentGltfTask.class.getSimpleName();
+ private final T renderable;
+ private final RenderableInternalFilamentAssetData renderableData;
+
+ LoadRenderableFromFilamentGltfTask(
+ T renderable, Context context, Uri sourceUri, @Nullable Function urlResolver) {
+ this.renderable = renderable;
+ IRenderableInternalData data = renderable.getRenderableData();
+ if (data instanceof RenderableInternalFilamentAssetData) {
+ this.renderableData =
+ (com.google.ar.sceneform.rendering.RenderableInternalFilamentAssetData) data;
+ } else {
+ throw new IllegalStateException("Expected task type " + TAG);
+ }
+ this.renderableData.resourceLoader =
+ new ResourceLoader(EngineInstance.getEngine().getFilamentEngine());
+ this.renderableData.urlResolver =
+ missingPath -> getUriFromMissingResource(sourceUri, missingPath, urlResolver);
+ this.renderableData.context = context.getApplicationContext();
+ this.renderable.getId().update();
+ }
+
+ /** Returns {@link CompletableFuture} for a new {@link Renderable}. */
+ @SuppressWarnings({"AndroidApiChecker"})
+ public CompletableFuture downloadAndProcessRenderable(
+ Callable inputStreamCreator) {
+
+ return CompletableFuture.supplyAsync(
+ // Download byte buffer via thread pool
+ () -> {
+ try {
+ return SceneformBufferUtils.inputStreamCallableToByteArray(inputStreamCreator);
+ } catch (Exception e) {
+ throw new CompletionException(e);
+ }
+ },
+ ThreadPools.getThreadPoolExecutor())
+ .thenApplyAsync(
+ gltfByteBuffer -> {
+ // Check for glb header
+ this.renderableData.isGltfBinary = gltfByteBuffer[0] == 0x67
+ && gltfByteBuffer[1] == 0x6C
+ && gltfByteBuffer[2] == 0x54
+ && gltfByteBuffer[3] == 0x46;
+ this.renderableData.gltfByteBuffer = ByteBuffer.wrap(gltfByteBuffer);
+ return renderable;
+ },
+ ThreadPools.getMainExecutor());
+ }
+
+ @NonNull
+ static Uri getUriFromMissingResource(
+ @NonNull Uri parentUri,
+ @NonNull String missingResource,
+ @Nullable Function urlResolver) {
+
+ if (urlResolver != null) {
+ return urlResolver.apply(missingResource);
+ }
+
+ if (missingResource.startsWith("/")) {
+ missingResource = missingResource.substring(1);
+ }
+
+ // Ensure encoding.
+ Uri decodedMissingResUri = Uri.parse(Uri.decode(missingResource));
+
+ if (decodedMissingResUri.getScheme() != null) {
+ throw new AssertionError(
+ String.format(
+ "Resource path contains a scheme but should be relative, uri: (%s)",
+ decodedMissingResUri));
+ }
+
+ // Build uri to missing resource.
+ String decodedMissingResPath = Preconditions.checkNotNull(decodedMissingResUri.getPath());
+ Uri decodedParentUri = Uri.parse(Uri.decode(parentUri.toString()));
+ Uri uri = decodedParentUri.buildUpon().appendPath("..").appendPath(decodedMissingResPath).build();
+ // Normalize and return Uri.
+ return Uri.parse(Uri.decode(URI.create(uri.toString()).normalize().toString()));
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LoadRenderableFromSfbTask.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LoadRenderableFromSfbTask.java
new file mode 100644
index 0000000..83e1922
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LoadRenderableFromSfbTask.java
@@ -0,0 +1,764 @@
+package com.google.ar.sceneform.rendering;
+
+import android.net.Uri;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import com.google.android.filament.IndexBuffer;
+import com.google.android.filament.TextureSampler;
+import com.google.android.filament.VertexBuffer;
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.SceneformBundle.VersionException;
+import com.google.ar.sceneform.utilities.Preconditions;
+import com.google.ar.sceneform.utilities.SceneformBufferUtils;
+import com.google.ar.schemas.lull.ModelDef;
+import com.google.ar.schemas.lull.ModelIndexRange;
+import com.google.ar.schemas.lull.ModelInstanceDef;
+
+import com.google.ar.schemas.lull.Vec3;
+import com.google.ar.schemas.lull.VertexAttribute;
+import com.google.ar.schemas.lull.VertexAttributeType;
+import com.google.ar.schemas.lull.VertexAttributeUsage;
+import com.google.ar.schemas.sceneform.BoolInit;
+import com.google.ar.schemas.sceneform.BoolVec2Init;
+import com.google.ar.schemas.sceneform.BoolVec3Init;
+import com.google.ar.schemas.sceneform.BoolVec4Init;
+import com.google.ar.schemas.sceneform.CompiledMaterialDef;
+import com.google.ar.schemas.sceneform.IntInit;
+import com.google.ar.schemas.sceneform.IntVec2Init;
+import com.google.ar.schemas.sceneform.IntVec3Init;
+import com.google.ar.schemas.sceneform.IntVec4Init;
+import com.google.ar.schemas.sceneform.ParameterDef;
+import com.google.ar.schemas.sceneform.ParameterInitDef;
+import com.google.ar.schemas.sceneform.ParameterInitDefType;
+import com.google.ar.schemas.sceneform.SamplerDef;
+import com.google.ar.schemas.sceneform.SamplerInit;
+import com.google.ar.schemas.sceneform.ScalarInit;
+import com.google.ar.schemas.sceneform.SceneformBundleDef;
+import com.google.ar.schemas.sceneform.TransformDef;
+import com.google.ar.schemas.sceneform.Vec2Init;
+import com.google.ar.schemas.sceneform.Vec3Init;
+import com.google.ar.schemas.sceneform.Vec4Init;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+/** Task for initializing a Renderable with data from an SFB. */
+@SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"}) // CompletableFuture
+class LoadRenderableFromSfbTask {
+ private static class ModelTexture {
+ String name;
+ @Nullable Texture data;
+
+ ModelTexture(String name) {
+ this.name = name;
+ this.data = null;
+ }
+ }
+
+ private static final String TAG = LoadRenderableFromSfbTask.class.getSimpleName();
+ private final T renderable;
+ private final RenderableInternalData renderableData;
+ @Nullable private final Uri renderableUri;
+
+ private ModelDef modelDef;
+ private ModelInstanceDef modelInstanceDef;
+ private TransformDef transformDef;
+
+ private int meshCount;
+ private int textureCount;
+
+ private int vertexCount;
+ private int vertexStride;
+
+ private int indexCount;
+ private IndexBuffer.Builder.IndexType indexType;
+ private ByteBuffer vertexBufferData;
+ private ByteBuffer indexBufferData;
+
+ private final ArrayList textures = new ArrayList<>();
+ private final ArrayList compiledMaterials = new ArrayList<>();
+ private final ArrayList compiledMaterialIndex = new ArrayList<>();
+ private final ArrayList materialParameters = new ArrayList<>();
+ private final ArrayList materialNames = new ArrayList<>();
+
+ private static final int BYTES_PER_FLOAT = Float.SIZE / 8;
+ private static final int BYTES_PER_SHORT = 2;
+ private static final int BYTES_PER_INT = 4;
+
+ LoadRenderableFromSfbTask(T renderable, @Nullable Uri renderableUri) {
+ this.renderable = renderable;
+ IRenderableInternalData data = renderable.getRenderableData();
+ if (data instanceof RenderableInternalData) {
+ this.renderableData = (RenderableInternalData) data;
+ } else {
+ throw new IllegalStateException("Expected task type " + TAG);
+ }
+ this.renderableUri = renderableUri;
+ }
+
+ /**
+ * @param inputStreamCreator supplies {@link Renderable} in serialized format
+ * @return {@link CompletableFuture} for a new {@link Renderable}
+ */
+ public CompletableFuture downloadAndProcessRenderable(
+ Callable inputStreamCreator) {
+
+ CompletableFuture result =
+ CompletableFuture.supplyAsync(
+ // Download byte buffer via thread pool
+ () -> {
+ ByteBuffer assetData =
+ SceneformBufferUtils.inputStreamToByteBuffer(inputStreamCreator);
+
+ // Parse byte buffer via thread pool
+ SceneformBundleDef sfb = byteBufferToSfb(assetData);
+ setCollisionShape(sfb);
+ // Create sub-assets including material parameters, textures and geometry
+ loadModel(sfb);
+ return sfb;
+ },
+ ThreadPools.getThreadPoolExecutor())
+ .thenComposeAsync(
+ sfb -> {
+ loadAnimations(sfb);
+
+ // Load textures and wait for them to finish.
+ return loadTexturesAsync(sfb);
+ },
+ ThreadPools.getMainExecutor())
+ .thenApplyAsync(
+ sfb -> {
+ // Fill in the material parameters. could be done on another thread, but kept here
+ // to reduce switching.
+ buildMaterialParameters(sfb);
+ return setupFilament(sfb);
+ },
+ ThreadPools.getMainExecutor());
+
+ result.exceptionally(
+ // Log Exception if there was one.
+ throwable -> {
+ throw new CompletionException(throwable);
+ });
+
+ return result;
+ }
+
+
+ private void loadAnimations(SceneformBundleDef sfb) {return ;}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private SceneformBundleDef byteBufferToSfb(ByteBuffer assetData) {
+ try {
+ SceneformBundleDef sfb;
+ sfb = SceneformBundle.tryLoadSceneformBundle(assetData);
+ if (sfb != null) {
+ return sfb;
+ }
+ } catch (VersionException e) {
+ throw new CompletionException(e);
+ }
+ throw new AssertionError("No RCB file at uri: " + renderableUri);
+ }
+
+ private SceneformBundleDef setCollisionShape(SceneformBundleDef sfb) {
+ try {
+ renderable.collisionShape = SceneformBundle.readCollisionGeometry(sfb);
+ return sfb;
+ } catch (IOException e) {
+ throw new CompletionException("Unable to get collision geometry from sfb", e);
+ }
+ }
+
+ private SceneformBundleDef loadModel(SceneformBundleDef sfb) {
+ // Prepare the flatbuffer data
+ transformDef = sfb.transform();
+ modelDef = sfb.model();
+ Preconditions.checkNotNull(modelDef, "Model error: ModelDef is invalid.");
+
+ modelInstanceDef = modelDef.lods(0);
+ Preconditions.checkNotNull(modelInstanceDef, "Lull Model error: ModelInstanceDef is invalid.");
+
+ // The data buffers for Geometry have to stick around anyway, so go ahead and load them
+ // now. The Filament buffers will be created in createAssetFromBuffer()
+ buildGeometry();
+ return sfb;
+ }
+
+ private T setupFilament(SceneformBundleDef sfb) {
+ Preconditions.checkNotNull(sfb);
+ setupFilamentGeometryBuffers();
+ setupFilamentMaterials(sfb);
+ setupRenderableData();
+ renderable.getId().update();
+ return renderable;
+ }
+
+ private void setupFilamentGeometryBuffers() {
+ IEngine engine = EngineInstance.getEngine();
+
+ IndexBuffer indexBuffer =
+ new IndexBuffer.Builder()
+ .indexCount(indexCount)
+ .bufferType(indexType)
+ .build(engine.getFilamentEngine());
+ indexBuffer.setBuffer(engine.getFilamentEngine(), indexBufferData);
+ renderableData.setIndexBuffer(indexBuffer);
+
+ VertexBuffer.Builder vertexBufferBuilder =
+ new VertexBuffer.Builder().vertexCount(vertexCount).bufferCount(1);
+
+ int vertexAttributeCount = modelInstanceDef.vertexAttributesLength();
+ int byteOffset = 0;
+ for (int i = 0; i < vertexAttributeCount; i++) {
+ VertexAttribute attribute = modelInstanceDef.vertexAttributes(i);
+ VertexBuffer.VertexAttribute filamentAttribute =
+ getFilamentVertexAttribute(attribute.usage());
+ if (filamentAttribute != null) {
+ vertexBufferBuilder.attribute(
+ filamentAttribute,
+ 0,
+ getFilamentAttributeType(attribute.type()),
+ byteOffset,
+ vertexStride);
+ if (isAttributeNormalized(attribute.usage())) {
+ vertexBufferBuilder.normalized(filamentAttribute);
+ }
+ }
+
+ byteOffset += getVertexAttributeTypeSizeInBytes(attribute.type());
+ }
+
+ VertexBuffer vertexBuffer = vertexBufferBuilder.build(engine.getFilamentEngine());
+ vertexBuffer.setBufferAt(engine.getFilamentEngine(), 0, vertexBufferData);
+ renderableData.setVertexBuffer(vertexBuffer);
+
+ setupAnimation();
+ }
+
+
+ private void setupAnimation() {return ;}
+
+
+
+
+
+
+
+
+
+ private void setupFilamentMaterials(SceneformBundleDef sfb) {
+ int compiledMaterialLength = sfb.compiledMaterialsLength();
+
+ for (int i = 0; i < compiledMaterialLength; ++i) {
+ CompiledMaterialDef compiledMaterial = sfb.compiledMaterials(i);
+
+ // If the same material buffer exists in multiple places this will ensure we
+ // only load it into graphics memory once.
+ int materialId = compiledMaterial.compiledMaterialAsByteBuffer().hashCode();
+
+ // use the registry to get the material or create it if needed
+ ByteBuffer copy;
+ try {
+ copy = SceneformBufferUtils.copyByteBuffer(compiledMaterial.compiledMaterialAsByteBuffer());
+ } catch (IOException e) {
+ throw new CompletionException("Failed to create material", e);
+ }
+
+ CompletableFuture materialFuture =
+ Material.builder().setSource(copy).setRegistryId(materialId).build();
+
+ @SuppressWarnings("nullness")
+ Material material = materialFuture.getNow(null);
+
+ // Material should always be loaded immediately because the source is a raw byte buffer.
+ if (material == null) {
+ throw new AssertionError("Material wasn't loaded.");
+ }
+
+ compiledMaterials.add(material);
+ }
+ }
+
+ private void setupRenderableData() {
+ // Get the bounds.
+ final Vec3 modelMinAabb = modelDef.boundingBox().min();
+ final Vector3 minAabb = new Vector3(modelMinAabb.x(), modelMinAabb.y(), modelMinAabb.z());
+ final Vec3 modelMaxAabb = modelDef.boundingBox().max();
+ final Vector3 maxAabb = new Vector3(modelMaxAabb.x(), modelMaxAabb.y(), modelMaxAabb.z());
+ Vector3 extentsAabb = Vector3.subtract(maxAabb, minAabb).scaled(0.5f);
+ Vector3 centerAabb = Vector3.add(minAabb, extentsAabb);
+ renderableData.setExtentsAabb(extentsAabb);
+ renderableData.setCenterAabb(centerAabb);
+ // Finding a scale of 0 indicates a default-initialized (i.e. invalid) structure.
+ if (transformDef != null && transformDef.scale() != 0.0f) {
+ Vec3 modelOffset = transformDef.offset();
+ Vector3 offset = new Vector3(modelOffset.x(), modelOffset.y(), modelOffset.z());
+ renderableData.setTransformScale(transformDef.scale());
+ renderableData.setTransformOffset(offset);
+ }
+
+ ArrayList materialBindings = renderable.getMaterialBindings();
+ ArrayList renderableMaterialNames = renderable.getMaterialNames();
+ materialBindings.clear();
+ renderableMaterialNames.clear();
+ for (int m = 0; m < meshCount; ++m) {
+ final ModelIndexRange range = modelInstanceDef.ranges(m);
+ final int start = (int) range.start();
+ final int end = (int) range.end();
+
+ int materialIndex = compiledMaterialIndex.get(m);
+ Material material = compiledMaterials.get(materialIndex).makeCopy();
+ MaterialParameters params = materialParameters.get(m);
+ material.copyMaterialParameters(params);
+
+ RenderableInternalData.MeshData meshData = new RenderableInternalData.MeshData();
+ materialBindings.add(material);
+ renderableMaterialNames.add(materialNames.get(m));
+ meshData.indexStart = start;
+ meshData.indexEnd = end;
+ renderableData.getMeshes().add(meshData);
+ }
+ }
+
+ private void buildGeometry() {
+ ByteBuffer vertexData = modelInstanceDef.vertexDataAsByteBuffer();
+
+ Preconditions.checkNotNull(
+ vertexData, "Model Instance geometry data is invalid (vertexData is null).");
+
+ int vertexDataCount = modelInstanceDef.vertexDataLength();
+ meshCount = modelInstanceDef.rangesLength();
+
+ int bytesPerVertex = LullModel.getByteCountPerVertex(modelInstanceDef);
+ vertexCount = vertexDataCount / bytesPerVertex;
+
+ // TODO: Fix crash in filament when using flatbuffer buffers directly.
+ if (modelInstanceDef.indices32Length() > 0) {
+ // 32 bit indices
+ indexCount = modelInstanceDef.indices32Length();
+ indexType = IndexBuffer.Builder.IndexType.UINT;
+ indexBufferData = ByteBuffer.allocateDirect(indexCount * BYTES_PER_INT);
+ indexBufferData.put(modelInstanceDef.indices32AsByteBuffer());
+ } else if (modelInstanceDef.indices16Length() > 0) {
+ // 16 bit indices
+ indexCount = modelInstanceDef.indices16Length();
+ indexType = IndexBuffer.Builder.IndexType.USHORT;
+ indexBufferData = ByteBuffer.allocateDirect(indexCount * BYTES_PER_SHORT);
+ indexBufferData.put(modelInstanceDef.indices16AsByteBuffer());
+ } else {
+ throw new AssertionError(
+ "Model Instance geometry data is invalid (model has no index data).");
+ }
+ indexBufferData.flip();
+
+ vertexBufferData = ByteBuffer.allocateDirect(vertexData.remaining());
+ Preconditions.checkNotNull(vertexBufferData, "Failed to allocate geometry for FilamentModel.");
+
+ vertexBufferData.put(vertexData);
+ vertexBufferData.flip();
+
+ // Calculate vertex stride
+ vertexStride = 0;
+ int vertexAttributeCount = modelInstanceDef.vertexAttributesLength();
+ for (int i = 0; i < vertexAttributeCount; i++) {
+ VertexAttribute attribute = modelInstanceDef.vertexAttributes(i);
+ vertexStride += getVertexAttributeTypeSizeInBytes(attribute.type());
+
+ // TODO: check all attributes available.
+ }
+ }
+
+ // TODO: Return a future for all texture loads, use theComposeAsync to
+ // combine it in downloadAndProcessRenderable
+ private CompletableFuture loadTexturesAsync(SceneformBundleDef sfb) {
+ textureCount = sfb.samplersLength();
+
+ CompletableFuture>[] textureFutures = new CompletableFuture>[textureCount];
+
+ for (int t = 0; t < textureCount; ++t) {
+ final SamplerDef samplerDef = sfb.samplers(t);
+ ModelTexture texture = new ModelTexture(samplerDef.name());
+ textures.add(texture);
+ CompletableFuture textureFuture = null;
+
+ int rawUsage = samplerDef.params().usageType();
+ Texture.Usage[] usageValues = Texture.Usage.values();
+ if (rawUsage >= usageValues.length) {
+ throw new AssertionError("Invalid Texture Usage: " + rawUsage);
+ }
+ Texture.Usage usage = usageValues[rawUsage];
+
+ if (samplerDef.dataLength() != 0) {
+ // loading texture from RCB
+ ByteBuffer data = samplerDef.dataAsByteBuffer();
+ // BUG(b/74619992): An extra copy to input stream is made here to avoid a JNI crash
+ ByteArrayInputStream wrappedInputStream =
+ new ByteArrayInputStream(data.array(), data.arrayOffset(), data.capacity());
+ // position the stream to the image buffer
+ boolean premultiplyAlpha = (usage == Texture.Usage.COLOR);
+ wrappedInputStream.skip(data.position());
+ // TODO: The registryId should be populated with a sha1sum
+
+ textureFuture =
+ Texture.builder()
+ .setUsage(usage)
+ .setSampler(samplerDefToSampler(samplerDef))
+ .setPremultiplied(premultiplyAlpha)
+ .setSource(
+ () -> {
+ Preconditions.checkNotNull(wrappedInputStream);
+ return wrappedInputStream;
+ })
+ .build();
+ } else {
+ throw new IllegalStateException("Unable to load texture, no sampler definition.");
+ }
+
+ textureFutures[t] =
+ textureFuture
+ .thenAccept(textureData -> texture.data = textureData)
+ .exceptionally(
+ throwable -> {
+ throw new CompletionException("Texture Load Error", throwable);
+ });
+ }
+
+ CompletableFuture allTexturesFuture = CompletableFuture.allOf(textureFutures);
+
+ return allTexturesFuture.thenApply((unused) -> sfb);
+ }
+
+ private static Texture.Sampler samplerDefToSampler(SamplerDef samplerDef) {
+ Texture.Sampler.WrapMode wrapModeR =
+ filamentWrapModeToWrapMode(TextureSampler.WrapMode.values()[samplerDef.params().wrapR()]);
+ Texture.Sampler.WrapMode wrapModeS =
+ filamentWrapModeToWrapMode(TextureSampler.WrapMode.values()[samplerDef.params().wrapS()]);
+ Texture.Sampler.WrapMode wrapModeT =
+ filamentWrapModeToWrapMode(TextureSampler.WrapMode.values()[samplerDef.params().wrapT()]);
+
+ return Texture.Sampler.builder()
+ .setMinFilter(samplerDefToMinFilter(samplerDef))
+ .setMagFilter(samplerDefToMagFilter(samplerDef))
+ .setWrapModeR(wrapModeR)
+ .setWrapModeS(wrapModeS)
+ .setWrapModeT(wrapModeT)
+ .build();
+ }
+
+ private SceneformBundleDef buildMaterialParameters(SceneformBundleDef sfb) {
+ int materialsCount = sfb.materialsLength();
+ if (materialsCount == 0) {
+ Log.i(TAG, "Building materials but the sceneform bundle has no materials");
+ return sfb;
+ }
+
+ for (int m = 0; m < meshCount; ++m) {
+
+ // material to submesh mapping is generally 1:1
+ int materialIndex = m;
+ // if submesh count exceeds the material count
+ // use that last material
+ if (materialsCount <= m) {
+ materialIndex = materialsCount - 1;
+ }
+
+ com.google.ar.schemas.sceneform.MaterialDef materialDef = sfb.materials(materialIndex);
+
+ if (materialDef == null) {
+ Log.e(TAG, "Material " + m + " is null.");
+ continue;
+ }
+
+ // map the parameters to the compiled material
+ compiledMaterialIndex.add(materialDef.compiledIndex());
+
+ // flatbuffers supports in-place methods for getting values,
+ // creating cache to hold those values before copying to parameter
+ ParameterDef parameterCache = new ParameterDef();
+ ParameterInitDef parameterInitCache = new ParameterInitDef();
+ ScalarInit scalarCache = new ScalarInit();
+ Vec2Init vec2Cache = new Vec2Init();
+ Vec3Init vec3Cache = new Vec3Init();
+ Vec4Init vec4Cache = new Vec4Init();
+ BoolInit boolCache = new BoolInit();
+ BoolVec2Init bool2Cache = new BoolVec2Init();
+ BoolVec3Init bool3Cache = new BoolVec3Init();
+ BoolVec4Init bool4Cache = new BoolVec4Init();
+ IntInit intCache = new IntInit();
+ IntVec2Init int2Cache = new IntVec2Init();
+ IntVec3Init int3Cache = new IntVec3Init();
+ IntVec4Init int4Cache = new IntVec4Init();
+ SamplerInit samplerCache = new SamplerInit();
+
+ MaterialParameters materialParameters = new MaterialParameters();
+
+ int paramCount = materialDef.parametersLength();
+ for (int i = 0; i < paramCount; ++i) {
+ materialDef.parameters(parameterCache, i);
+ parameterCache.initialValue(parameterInitCache);
+
+ String id = parameterCache.id();
+ byte parameterType = parameterInitCache.initType();
+ switch (parameterType) {
+ case ParameterInitDefType.NullInit:
+ // Nothing to do
+ break;
+ case ParameterInitDefType.ScalarInit:
+ parameterInitCache.init(scalarCache);
+ materialParameters.setFloat(id, scalarCache.value());
+ break;
+ case ParameterInitDefType.Vec2Init:
+ parameterInitCache.init(vec2Cache);
+ materialParameters.setFloat2(id, vec2Cache.x(), vec2Cache.y());
+ break;
+ case ParameterInitDefType.Vec3Init:
+ parameterInitCache.init(vec3Cache);
+ materialParameters.setFloat3(id, vec3Cache.x(), vec3Cache.y(), vec3Cache.z());
+ break;
+ case ParameterInitDefType.Vec4Init:
+ parameterInitCache.init(vec4Cache);
+ materialParameters.setFloat4(
+ id, vec4Cache.x(), vec4Cache.y(), vec4Cache.z(), vec4Cache.w());
+ break;
+ case ParameterInitDefType.BoolInit:
+ parameterInitCache.init(boolCache);
+ materialParameters.setBoolean(id, boolCache.value());
+ break;
+ case ParameterInitDefType.BoolVec2Init:
+ parameterInitCache.init(bool2Cache);
+ materialParameters.setBoolean2(id, bool2Cache.x(), bool2Cache.y());
+ break;
+ case ParameterInitDefType.BoolVec3Init:
+ parameterInitCache.init(bool3Cache);
+ materialParameters.setBoolean3(id, bool3Cache.x(), bool3Cache.y(), bool3Cache.z());
+ break;
+ case ParameterInitDefType.BoolVec4Init:
+ parameterInitCache.init(bool4Cache);
+ materialParameters.setBoolean4(
+ id, bool4Cache.x(), bool4Cache.y(), bool4Cache.z(), bool4Cache.w());
+ break;
+ case ParameterInitDefType.IntInit:
+ parameterInitCache.init(intCache);
+ materialParameters.setInt(id, intCache.value());
+ break;
+ case ParameterInitDefType.IntVec2Init:
+ parameterInitCache.init(int2Cache);
+ materialParameters.setInt2(id, int2Cache.x(), int2Cache.y());
+ break;
+ case ParameterInitDefType.IntVec3Init:
+ parameterInitCache.init(int3Cache);
+ materialParameters.setInt3(id, int3Cache.x(), int3Cache.y(), int3Cache.z());
+ break;
+ case ParameterInitDefType.IntVec4Init:
+ parameterInitCache.init(int4Cache);
+ materialParameters.setInt4(
+ id, int4Cache.x(), int4Cache.y(), int4Cache.z(), int4Cache.w());
+ break;
+ case ParameterInitDefType.SamplerInit:
+ parameterInitCache.init(samplerCache);
+ String path = samplerCache.path();
+ Texture texture = getTextureByName(path);
+ if (texture != null) {
+ materialParameters.setTexture(id, texture);
+ }
+ break;
+ case ParameterInitDefType.ExternalSamplerInit:
+ // No-op; handled externally from this loader.
+ break;
+ default:
+ Log.e(TAG, "Unknown parameter type: " + id);
+ }
+ }
+
+ this.materialParameters.add(materialParameters);
+ String materialName = materialDef.name();
+ this.materialNames.add(materialName != null ? materialName : "");
+ }
+ return sfb;
+ }
+
+ @Nullable
+ private Texture getTextureByName(String name) {
+ for (int t = 0; t < textureCount; ++t) {
+ if (Objects.equals(name, textures.get(t).name)) {
+ return textures.get(t).data;
+ }
+ }
+ return null;
+ }
+
+ private static int getVertexAttributeTypeSizeInBytes(int attributeType) {
+ int sizeInBytes = 0;
+ switch (attributeType) {
+ case VertexAttributeType.Empty:
+ sizeInBytes = 0;
+ break;
+ case VertexAttributeType.Scalar1f:
+ sizeInBytes = BYTES_PER_FLOAT;
+ break;
+ case VertexAttributeType.Vec2f:
+ sizeInBytes = 2 * BYTES_PER_FLOAT;
+ break;
+ case VertexAttributeType.Vec3f:
+ sizeInBytes = 3 * BYTES_PER_FLOAT;
+ break;
+ case VertexAttributeType.Vec4f:
+ sizeInBytes = 4 * BYTES_PER_FLOAT;
+ break;
+ case VertexAttributeType.Vec2us:
+ sizeInBytes = 2 * BYTES_PER_SHORT;
+ break;
+ case VertexAttributeType.Vec4us:
+ sizeInBytes = 4 * BYTES_PER_SHORT;
+ break;
+ case VertexAttributeType.Vec4ub:
+ sizeInBytes = 4;
+ break;
+ default:
+ throw new AssertionError("Unsupported VertexAttributeType value: " + attributeType);
+ }
+ return sizeInBytes;
+ }
+
+ private boolean isAttributeNormalized(int attributeUsage) {
+ return attributeUsage == VertexAttributeUsage.Color
+ || attributeUsage == VertexAttributeUsage.BoneWeights;
+ }
+
+ @Nullable
+ private static VertexBuffer.VertexAttribute getFilamentVertexAttribute(int attributeUsage) {
+ VertexBuffer.VertexAttribute filamentAttribute;
+ switch (attributeUsage) {
+ case VertexAttributeUsage.Position:
+ filamentAttribute = VertexBuffer.VertexAttribute.POSITION;
+ break;
+ case VertexAttributeUsage.Color:
+ filamentAttribute = VertexBuffer.VertexAttribute.COLOR;
+ break;
+ case VertexAttributeUsage.TexCoord:
+ filamentAttribute = VertexBuffer.VertexAttribute.UV0;
+ break;
+ case VertexAttributeUsage.Orientation:
+ filamentAttribute = VertexBuffer.VertexAttribute.TANGENTS;
+ break;
+ case VertexAttributeUsage.BoneIndices:
+ filamentAttribute = VertexBuffer.VertexAttribute.BONE_INDICES;
+ break;
+ case VertexAttributeUsage.BoneWeights:
+ filamentAttribute = VertexBuffer.VertexAttribute.BONE_WEIGHTS;
+ break;
+ default:
+ filamentAttribute = null;
+ break;
+ }
+ return filamentAttribute;
+ }
+
+ private static VertexBuffer.AttributeType getFilamentAttributeType(int attributeType) {
+ VertexBuffer.AttributeType filamentAttributeType;
+ switch (attributeType) {
+ case VertexAttributeType.Scalar1f:
+ filamentAttributeType = VertexBuffer.AttributeType.FLOAT;
+ break;
+ case VertexAttributeType.Vec2f:
+ filamentAttributeType = VertexBuffer.AttributeType.FLOAT2;
+ break;
+ case VertexAttributeType.Vec3f:
+ filamentAttributeType = VertexBuffer.AttributeType.FLOAT3;
+ break;
+ case VertexAttributeType.Vec4f:
+ filamentAttributeType = VertexBuffer.AttributeType.FLOAT4;
+ break;
+ case VertexAttributeType.Vec2us:
+ filamentAttributeType = VertexBuffer.AttributeType.USHORT2;
+ break;
+ case VertexAttributeType.Vec4us:
+ filamentAttributeType = VertexBuffer.AttributeType.USHORT4;
+ break;
+ case VertexAttributeType.Vec4ub:
+ filamentAttributeType = VertexBuffer.AttributeType.UBYTE4;
+ break;
+ default:
+ throw new AssertionError("Unsupported VertexAttributeType value: " + attributeType);
+ }
+ return filamentAttributeType;
+ }
+
+ private static Texture.Sampler.MagFilter samplerDefToMagFilter(SamplerDef samplerDef) {
+ TextureSampler.MagFilter filamentMagFilter =
+ TextureSampler.MagFilter.values()[samplerDef.params().magFilter()];
+
+ switch (filamentMagFilter) {
+ case NEAREST:
+ return Texture.Sampler.MagFilter.NEAREST;
+ case LINEAR:
+ return Texture.Sampler.MagFilter.LINEAR;
+ }
+ throw new IllegalArgumentException("Invalid MagFilter");
+ }
+
+ private static Texture.Sampler.MinFilter samplerDefToMinFilter(SamplerDef samplerDef) {
+ TextureSampler.MinFilter filamentMinFilter =
+ TextureSampler.MinFilter.values()[samplerDef.params().minFilter()];
+
+ switch (filamentMinFilter) {
+ case NEAREST:
+ return Texture.Sampler.MinFilter.NEAREST;
+ case LINEAR:
+ return Texture.Sampler.MinFilter.LINEAR;
+ case NEAREST_MIPMAP_NEAREST:
+ return Texture.Sampler.MinFilter.NEAREST_MIPMAP_NEAREST;
+ case LINEAR_MIPMAP_NEAREST:
+ return Texture.Sampler.MinFilter.LINEAR_MIPMAP_NEAREST;
+ case NEAREST_MIPMAP_LINEAR:
+ return Texture.Sampler.MinFilter.NEAREST_MIPMAP_LINEAR;
+ case LINEAR_MIPMAP_LINEAR:
+ return Texture.Sampler.MinFilter.LINEAR_MIPMAP_LINEAR;
+ }
+ throw new IllegalArgumentException("Invalid MinFilter");
+ }
+
+ private static Texture.Sampler.WrapMode filamentWrapModeToWrapMode(
+ TextureSampler.WrapMode wrapMode) {
+ switch (wrapMode) {
+ case CLAMP_TO_EDGE:
+ return Texture.Sampler.WrapMode.CLAMP_TO_EDGE;
+ case REPEAT:
+ return Texture.Sampler.WrapMode.REPEAT;
+ case MIRRORED_REPEAT:
+ return Texture.Sampler.WrapMode.MIRRORED_REPEAT;
+ }
+ throw new IllegalArgumentException("Invalid WrapMode");
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LullModel.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LullModel.java
new file mode 100644
index 0000000..eea614e
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/LullModel.java
@@ -0,0 +1,106 @@
+package com.google.ar.sceneform.rendering;
+
+import android.util.Log;
+import com.google.android.filament.TextureSampler.MagFilter;
+import com.google.android.filament.TextureSampler.MinFilter;
+import com.google.android.filament.TextureSampler.WrapMode;
+import com.google.ar.schemas.lull.ModelInstanceDef;
+import com.google.ar.schemas.lull.TextureFiltering;
+import com.google.ar.schemas.lull.VertexAttribute;
+import com.google.ar.schemas.lull.VertexAttributeType;
+import java.nio.ByteBuffer;
+
+/**
+ * Helper functions for loading and processing lull models.
+ *
+ * @hide
+ */
+public class LullModel {
+ private static final String TAG = LullModel.class.getName();
+
+ // map from lull wrap mode to filament wrap mode
+ public static final WrapMode[] fromLullWrapMode =
+ new WrapMode[] {
+ WrapMode.CLAMP_TO_EDGE, // Maps from lull clamp-to-border (index 0)
+ WrapMode.CLAMP_TO_EDGE, // Maps from lull clamp-to-edge (index 0)
+ WrapMode.MIRRORED_REPEAT, // Maps from lull mirrored-repeat (index 2)
+ WrapMode.CLAMP_TO_EDGE, // Maps from lull mirrored-clamp-to-edge (index 0)
+ WrapMode.REPEAT, // Maps from lull repeat (index 4)
+ };
+
+ public static boolean isLullModel(ByteBuffer buffer) {
+ // LullModel header = 0x12, 0x00, 0x00, 0x00
+ final int lullModelHeaderLen = 4;
+ return buffer.limit() > lullModelHeaderLen
+ && buffer.get(0) < 32
+ && buffer.get(1) == 0x00
+ && buffer.get(2) == 0x00;
+ }
+
+ public static int getByteCountPerVertex(ModelInstanceDef modelInstanceDef) {
+ int vertexAttributeCount = modelInstanceDef.vertexAttributesLength();
+ int bytesPerVertex = 0;
+ for (int i = 0; i < vertexAttributeCount; i++) {
+ VertexAttribute attribute = modelInstanceDef.vertexAttributes(i);
+ switch (attribute.type()) {
+ case VertexAttributeType.Vec3f:
+ bytesPerVertex += 12;
+ break;
+ case VertexAttributeType.Vec4f:
+ bytesPerVertex += 16;
+ break;
+ case VertexAttributeType.Vec2f:
+ case VertexAttributeType.Vec4us:
+ bytesPerVertex += 8;
+ break;
+ case VertexAttributeType.Scalar1f:
+ case VertexAttributeType.Vec2us:
+ case VertexAttributeType.Vec4ub:
+ bytesPerVertex += 4;
+ break;
+ case VertexAttributeType.Empty:
+ default:
+ break;
+ }
+ }
+ return bytesPerVertex;
+ }
+
+ public static MinFilter fromLullToMinFilter(com.google.ar.schemas.lull.TextureDef textureDef) {
+ switch (textureDef.minFilter()) {
+ case TextureFiltering.Nearest:
+ return MinFilter.NEAREST;
+ case TextureFiltering.Linear:
+ return MinFilter.LINEAR;
+ case TextureFiltering.NearestMipmapNearest:
+ return MinFilter.NEAREST_MIPMAP_NEAREST;
+ case TextureFiltering.LinearMipmapNearest:
+ return MinFilter.LINEAR_MIPMAP_NEAREST;
+ case TextureFiltering.NearestMipmapLinear:
+ return MinFilter.NEAREST_MIPMAP_LINEAR;
+ case TextureFiltering.LinearMipmapLinear:
+ return MinFilter.LINEAR_MIPMAP_LINEAR;
+ default:
+ {
+ Log.e(TAG, textureDef.name() + ": Sampler has unknown min filter");
+ }
+ }
+
+ return MinFilter.NEAREST;
+ }
+
+ public static MagFilter fromLullToMagFilter(com.google.ar.schemas.lull.TextureDef textureDef) {
+ switch (textureDef.magFilter()) {
+ case TextureFiltering.Nearest:
+ return MagFilter.NEAREST;
+ case TextureFiltering.Linear:
+ return MagFilter.LINEAR;
+ default:
+ {
+ Log.e(TAG, textureDef.name() + ": Sampler has unknown mag filter");
+ }
+ }
+
+ return MagFilter.NEAREST;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Material.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Material.java
new file mode 100644
index 0000000..377e0c8
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Material.java
@@ -0,0 +1,654 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.android.filament.MaterialInstance;
+
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.resources.ResourceRegistry;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.LoadHelper;
+import com.google.ar.sceneform.utilities.Preconditions;
+import com.google.ar.sceneform.utilities.SceneformBufferUtils;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+/** Represents a reference to a material. */
+@RequiresApi(api = Build.VERSION_CODES.N)
+public class Material {
+ private static final String TAG = Material.class.getSimpleName();
+
+ private final MaterialParameters materialParameters = new MaterialParameters();
+ @Nullable private final MaterialInternalData materialData;
+ private final IMaterialInstance internalMaterialInstance;
+
+ /**
+ * Creates a new instance of this Material.
+ *
+ * The new material will have a unique copy of the material parameters that can be changed
+ * independently. The getFilamentEngine material resource is immutable and will be shared between
+ * instances.
+ */
+ public Material makeCopy() {
+ return new Material(this);
+ }
+
+
+
+
+
+
+ public void setBoolean(String name, boolean x) {
+ materialParameters.setBoolean(name, x);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+ public void setBoolean2(String name, boolean x, boolean y) {
+ materialParameters.setBoolean2(name, x, y);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ public void setBoolean3(String name, boolean x, boolean y, boolean z) {
+ materialParameters.setBoolean3(name, x, y, z);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ public void setBoolean4(String name, boolean x, boolean y, boolean z, boolean w) {
+ materialParameters.setBoolean4(name, x, y, z, w);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ public void setFloat(String name, float x) {
+ materialParameters.setFloat(name, x);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+ public void setFloat2(String name, float x, float y) {
+ materialParameters.setFloat2(name, x, y);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ public void setFloat3(String name, float x, float y, float z) {
+ materialParameters.setFloat3(name, x, y, z);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+ public void setFloat3(String name, Vector3 value) {
+ materialParameters.setFloat3(name, value);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+ public void setFloat3(String name, Color color) {
+ materialParameters.setFloat3(name, color.r, color.g, color.b);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ public void setFloat4(String name, float x, float y, float z, float w) {
+ materialParameters.setFloat4(name, x, y, z, w);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+ public void setFloat4(String name, Color color) {
+ materialParameters.setFloat4(name, color.r, color.g, color.b, color.a);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ public void setInt(String name, int x) {
+ materialParameters.setInt(name, x);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+ public void setInt2(String name, int x, int y) {
+ materialParameters.setInt2(name, x, y);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ public void setInt3(String name, int x, int y, int z) {
+ materialParameters.setInt3(name, x, y, z);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ public void setInt4(String name, int x, int y, int z, int w) {
+ materialParameters.setInt4(name, x, y, z, w);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ public void setTexture(String name, Texture texture) {
+ materialParameters.setTexture(name, texture);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+ /**
+ * Sets an {@link ExternalTexture} to a parameter of type 'samplerExternal' on this material.
+ *
+ * @param name the name of the parameter in the material
+ * @param externalTexture the texture to set
+ */
+ public void setExternalTexture(String name, ExternalTexture externalTexture) {
+ materialParameters.setExternalTexture(name, externalTexture);
+ if (internalMaterialInstance.isValidInstance()) {
+ materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+ @Nullable
+ public ExternalTexture getExternalTexture(String name) {
+ return materialParameters.getExternalTexture(name);
+ }
+
+ /**
+ * Constructs a {@link Material}
+ *
+ * @hide We do not support custom materials in version 1.0 and use a Material Factory to create
+ * new materials, so there is no need to expose a builder.
+ */
+ public static Builder builder() {
+ AndroidPreconditions.checkMinAndroidApiLevel();
+
+ return new Builder();
+ }
+
+ void copyMaterialParameters(MaterialParameters materialParameters) {
+ this.materialParameters.copyFrom(materialParameters);
+ if (internalMaterialInstance.isValidInstance()) {
+ this.materialParameters.applyTo(internalMaterialInstance.getInstance());
+ }
+ }
+
+
+
+
+
+
+
+
+
+ // LINT.IfChange(api)
+ private static native Object nGetMaterialParameters(long modelDataHandle, int materialIndex);
+ // LINT.ThenChange(
+ // //depot/google3/third_party/arcore/ar/sceneform/loader/model/model_material_jni.cc:api
+ // )
+
+ com.google.android.filament.MaterialInstance getFilamentMaterialInstance() {
+ // Filament Material Instance is only set to null when it is disposed or destroyed, so any
+ // usage after that point is an internal error.
+ if (!internalMaterialInstance.isValidInstance()) {
+ throw new AssertionError("Filament Material Instance is null.");
+ }
+ return internalMaterialInstance.getInstance();
+ }
+
+ @SuppressWarnings("initialization")
+ private Material(MaterialInternalData materialData) {
+ this.materialData = materialData;
+ materialData.retain();
+ if (materialData instanceof MaterialInternalDataImpl) {
+ // Do the legacy thing.
+ internalMaterialInstance =
+ new InternalMaterialInstance(materialData.getFilamentMaterial().createInstance());
+ } else {
+ // Do the glTF thing.
+ internalMaterialInstance = new InternalGltfMaterialInstance();
+ }
+
+ ResourceManager.getInstance()
+ .getMaterialCleanupRegistry()
+ .register(this, new CleanupCallback(internalMaterialInstance, materialData));
+ }
+
+ void updateGltfMaterialInstance(MaterialInstance instance) {
+ if (internalMaterialInstance instanceof InternalGltfMaterialInstance) {
+ ((InternalGltfMaterialInstance) internalMaterialInstance).setMaterialInstance(instance);
+ materialParameters.applyTo(instance);
+ }
+ }
+
+ @SuppressWarnings("initialization")
+ private Material(Material other) {
+ this(other.materialData);
+ copyMaterialParameters(other.materialParameters);
+ }
+ /**
+ * Builder for constructing a {@link Material}
+ *
+ * @hide We do not support custom materials in version 1.0 and use a Material Factory to create
+ * new materials, so there is no need to expose a builder.
+ */
+ public static final class Builder {
+ /** The {@link Material} will be constructed from the contents of this buffer */
+ @Nullable ByteBuffer sourceBuffer;
+ /** The {@link Material} will be constructed from the contents of this callable */
+ @Nullable private Callable inputStreamCreator;
+ /** The {@link Material} will be constructed from an existing filament material. */
+ com.google.android.filament.Material existingMaterial;
+
+ @Nullable private Object registryId;
+
+ /** Constructor for asynchronous building. The sourceBuffer will be read later. */
+ private Builder() {}
+
+ /**
+ * Allows a {@link Material} to be created with data.
+ *
+ * Construction will be immediate. Please use {@link #setRegistryId(Object)} to register this
+ * material for reuse.
+ *
+ * @param materialBuffer Sets the material data.
+ * @return {@link Builder} for chaining setup calls
+ */
+ public Builder setSource(ByteBuffer materialBuffer) {
+ // TODO: Determine if this should be added to the registry?
+ Preconditions.checkNotNull(materialBuffer, "Parameter \"materialBuffer\" was null.");
+
+ inputStreamCreator = null;
+ sourceBuffer = materialBuffer;
+ return this;
+ }
+
+ /**
+ * Allows a {@link Material} to be constructed from {@link Uri}. Construction will be
+ * asynchronous.
+ *
+ * @param context Sets the {@link Context} used for loading the resource
+ * @param sourceUri Sets a remote Uri or android resource Uri. The material will be added to the
+ * registry using the Uri. A previously registered material with the same Uri will be
+ * re-used.
+ * @return {@link Builder} for chaining setup calls
+ */
+ public Builder setSource(Context context, Uri sourceUri) {
+ Preconditions.checkNotNull(sourceUri, "Parameter \"sourceUri\" was null.");
+
+ registryId = sourceUri;
+ inputStreamCreator = LoadHelper.fromUri(context, sourceUri);
+ sourceBuffer = null;
+ return this;
+ }
+
+ /**
+ * Allows a {@link Material} to be constructed from resource.
+ *
+ *
Construction will be asynchronous.
+ *
+ * @param context Sets the {@link Context} used for loading the resource
+ * @param resource an android resource with raw type. A previously registered material with the
+ * same resource id will be re-used.
+ * @return {@link Builder} for chaining setup calls
+ */
+ public Builder setSource(Context context, int resource) {
+ registryId = context.getResources().getResourceName(resource);
+ inputStreamCreator = LoadHelper.fromResource(context, resource);
+ sourceBuffer = null;
+ return this;
+ }
+
+ /**
+ * Allows a {@link Material} to be constructed via callable function.
+ *
+ * @param inputStreamCreator Supplies an {@link InputStream} with the {@link Material} data
+ * @return {@link Builder} for chaining setup calls
+ */
+ public Builder setSource(Callable inputStreamCreator) {
+ Preconditions.checkNotNull(
+ inputStreamCreator, "Parameter \"sourceInputStreamCallable\" was null.");
+
+ this.inputStreamCreator = inputStreamCreator;
+ sourceBuffer = null;
+ return this;
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /**
+ * Allows a {@link Material} to be reused. If registryId is non-null it will be saved in a
+ * registry and the registry will be checked for this id before construction.
+ *
+ * @param registryId allows the function to be skipped and a previous material to be re-used
+ * @return {@link Builder} for chaining setup calls
+ */
+ public Builder setRegistryId(Object registryId) {
+ this.registryId = registryId;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link Material} based on the parameters set previously. A source must be
+ * specified.
+ */
+ @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
+ public CompletableFuture build() {
+ try {
+ checkPreconditions();
+ } catch (Throwable failedPrecondition) {
+ CompletableFuture result = new CompletableFuture<>();
+ result.completeExceptionally(failedPrecondition);
+ FutureHelper.logOnException(
+ TAG, result, "Unable to load Material registryId='" + registryId + "'");
+ return result;
+ }
+
+ // For static-analysis check.
+ Object registryId = this.registryId;
+ if (registryId != null) {
+ // See if a material has already been registered by this id, if so re-use it.
+ ResourceRegistry registry = ResourceManager.getInstance().getMaterialRegistry();
+ @Nullable CompletableFuture materialFuture = registry.get(registryId);
+ if (materialFuture != null) {
+ return materialFuture.thenApply(material -> material.makeCopy());
+ }
+ }
+
+ if (sourceBuffer != null) {
+ MaterialInternalDataImpl materialData =
+ new MaterialInternalDataImpl(createFilamentMaterial(sourceBuffer));
+ Material material = new Material(materialData);
+
+ // Register the new material in the registry.
+ if (registryId != null) {
+ ResourceRegistry registry = ResourceManager.getInstance().getMaterialRegistry();
+ registry.register(registryId, CompletableFuture.completedFuture(material));
+ }
+
+ CompletableFuture result = CompletableFuture.completedFuture(material.makeCopy());
+ FutureHelper.logOnException(
+ TAG, result, "Unable to load Material registryId='" + registryId + "'");
+ return result;
+ } else if (existingMaterial != null) {
+ MaterialInternalDataGltfImpl materialData =
+ new MaterialInternalDataGltfImpl(existingMaterial);
+ Material material = new Material(materialData);
+
+ // Register the new material in the registry.
+ if (registryId != null) {
+ ResourceRegistry registry = ResourceManager.getInstance().getMaterialRegistry();
+ // In this case register a copy of the material.
+ registry.register(registryId, CompletableFuture.completedFuture(material.makeCopy()));
+ }
+
+ // The current existing (in use) material is returned.
+ CompletableFuture result = CompletableFuture.completedFuture(material);
+ FutureHelper.logOnException(
+ TAG, result, "Unable to load Material registryId='" + registryId + "'");
+ return result;
+ }
+
+ // For static-analysis check. Must be final for the lambda to accept the parameter.
+ final Callable inputStreamCallable = this.inputStreamCreator;
+ if (inputStreamCallable == null) {
+ CompletableFuture result = new CompletableFuture<>();
+ result.completeExceptionally(new AssertionError("Input Stream Creator is null."));
+ return result;
+ }
+
+ CompletableFuture result =
+ CompletableFuture.supplyAsync(
+ () -> {
+ @Nullable ByteBuffer byteBuffer;
+ // Open and read the material file.
+ try (InputStream inputStream = inputStreamCallable.call()) {
+ byteBuffer = SceneformBufferUtils.readStream(inputStream);
+ } catch (Exception e) {
+ throw new CompletionException(e);
+ }
+
+ if (byteBuffer == null) {
+ throw new IllegalStateException("Unable to read data from input stream.");
+ }
+
+ return byteBuffer;
+ },
+ ThreadPools.getThreadPoolExecutor())
+ .thenApplyAsync(
+ byteBuffer -> {
+ MaterialInternalDataImpl materialData =
+ new MaterialInternalDataImpl(createFilamentMaterial(byteBuffer));
+ Material material = new Material(materialData);
+ return material;
+ },
+ ThreadPools.getMainExecutor());
+
+ if (registryId != null) {
+ ResourceRegistry registry = ResourceManager.getInstance().getMaterialRegistry();
+ registry.register(registryId, result);
+ }
+
+ return result.thenApply(material -> material.makeCopy());
+ }
+
+ private void checkPreconditions() {
+ AndroidPreconditions.checkUiThread();
+
+ if (!hasSource()) {
+ throw new AssertionError("Material must have a source.");
+ }
+ }
+
+ private Boolean hasSource() {
+ return inputStreamCreator != null || sourceBuffer != null || existingMaterial != null;
+ }
+
+ private com.google.android.filament.Material createFilamentMaterial(ByteBuffer sourceBuffer) {
+ try {
+ return new com.google.android.filament.Material.Builder()
+ .payload(sourceBuffer, sourceBuffer.limit())
+ .build(EngineInstance.getEngine().getFilamentEngine());
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to create material from source byte buffer.", e);
+ }
+ }
+ }
+
+ // Material.java's internal representation of a material instance.
+ interface IMaterialInstance {
+ MaterialInstance getInstance();
+
+ boolean isValidInstance();
+
+ void dispose();
+ }
+
+ // Represents a filament material instance created in Sceneform.
+ static class InternalMaterialInstance implements IMaterialInstance {
+ final MaterialInstance instance;
+
+ public InternalMaterialInstance(MaterialInstance instance) {
+ this.instance = instance;
+ }
+
+ @Override
+ public MaterialInstance getInstance() {
+ return instance;
+ }
+
+ @Override
+ public boolean isValidInstance() {
+ return instance != null;
+ }
+
+ @Override
+ public void dispose() {
+ IEngine engine = EngineInstance.getEngine();
+ if (engine != null && engine.isValid()) {
+ engine.destroyMaterialInstance(instance);
+ }
+ }
+ }
+
+ // A filament material instance created and managed in the native loader.
+ static class InternalGltfMaterialInstance implements IMaterialInstance {
+ MaterialInstance instance;
+
+ public InternalGltfMaterialInstance() {}
+
+ void setMaterialInstance(MaterialInstance instance) {
+ this.instance = instance;
+ }
+
+ @Override
+ public MaterialInstance getInstance() {
+ return Preconditions.checkNotNull(instance);
+ }
+
+ @Override
+ public boolean isValidInstance() {
+ return instance != null;
+ }
+
+ @Override
+ public void dispose() {
+ // Material is tracked natively.
+ }
+ }
+
+ /** Cleanup filament objects after garbage collection */
+ private static final class CleanupCallback implements Runnable {
+ @Nullable private final MaterialInternalData materialInternalData;
+ @Nullable private final IMaterialInstance materialInstance;
+
+ CleanupCallback(
+ @Nullable IMaterialInstance materialInstance,
+ @Nullable MaterialInternalData materialInternalData) {
+ this.materialInstance = materialInstance;
+ this.materialInternalData = materialInternalData;
+ }
+
+ @Override
+ public void run() {
+ AndroidPreconditions.checkUiThread();
+ if (materialInstance != null) {
+ materialInstance.dispose();
+ }
+
+ if (materialInternalData != null) {
+ materialInternalData.release();
+ }
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialFactory.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialFactory.java
new file mode 100644
index 0000000..2d024e4
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialFactory.java
@@ -0,0 +1,201 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import android.os.Build;
+import androidx.annotation.RequiresApi;
+
+import java.util.concurrent.CompletableFuture;
+
+/** Utility class used to construct default {@link Material}s. */
+@RequiresApi(api = Build.VERSION_CODES.N)
+
+public final class MaterialFactory {
+ /**
+ * Name of material parameter for controlling the color of {@link #makeOpaqueWithColor(Context,
+ * Color)} and {@link #makeTransparentWithColor(Context, Color)} materials.
+ *
+ * @see Material#setFloat3(String, Color)
+ * @see Material#setFloat4(String, Color)
+ */
+ public static final String MATERIAL_COLOR = "color";
+
+ /**
+ * Name of material parameter for controlling the texture of {@link
+ * #makeOpaqueWithTexture(Context, Texture)} and {@link #makeTransparentWithTexture(Context,
+ * Texture)} materials.
+ *
+ * @see Material#setTexture(String, Texture)
+ */
+ public static final String MATERIAL_TEXTURE = "texture";
+
+ /**
+ * Name of material parameter for controlling the metallic property of all {@link MaterialFactory}
+ * materials. The metallic property defines whether the surface is a metallic (conductor) or a
+ * non-metallic (dielectric) surface. This property should be used as a binary value, set to
+ * either 0 or 1. Intermediate values are only truly useful to create transitions between
+ * different types of surfaces when using textures. The default value is 0.
+ *
+ * @see Material#setFloat(String, float)
+ */
+ public static final String MATERIAL_METALLIC = "metallic";
+
+ /**
+ * Name of material parameter for controlling the roughness property of all {@link
+ * MaterialFactory} materials. The roughness property controls the perceived smoothness of the
+ * surface. When roughness is set to 0, the surface is perfectly smooth and highly glossy. The
+ * rougher a surface is, the “blurrier” the reflections are. The default value is 0.4.
+ *
+ * @see Material#setFloat(String, float)
+ */
+ public static final String MATERIAL_ROUGHNESS = "roughness";
+
+ /**
+ * Name of material parameter for controlling the reflectance property of all {@link
+ * MaterialFactory} materials. The reflectance property only affects non-metallic surfaces. This
+ * property can be used to control the specular intensity. This value is defined between 0 and 1
+ * and represents a remapping of a percentage of reflectance. The default value is 0.5.
+ *
+ * @see Material#setFloat(String, float)
+ */
+ public static final String MATERIAL_REFLECTANCE = "reflectance";
+
+ private static final float DEFAULT_METALLIC_PROPERTY = 0.0f;
+ private static final float DEFAULT_ROUGHNESS_PROPERTY = 0.4f;
+ private static final float DEFAULT_REFLECTANCE_PROPERTY = 0.5f;
+
+ /**
+ * Creates an opaque {@link Material} with the {@link Color} passed in. The {@link Color} can be
+ * modified by calling {@link Material#setFloat3(String, Color)} with {@link #MATERIAL_COLOR}. The
+ * metallicness, roughness, and reflectance can be modified using {@link Material#setFloat(String,
+ * float)}.
+ *
+ * @see #MATERIAL_METALLIC
+ * @see #MATERIAL_ROUGHNESS
+ * @see #MATERIAL_REFLECTANCE
+ * @param context a context used for loading the material resource
+ * @param color the color for the material to render
+ * @return material that will render the given color
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ // CompletableFuture requires api level 24
+ public static CompletableFuture makeOpaqueWithColor(Context context, Color color) {
+ CompletableFuture materialFuture =
+ Material.builder()
+ .setSource(
+ context,
+ RenderingResources.GetSceneformResource(
+ context, RenderingResources.Resource.OPAQUE_COLORED_MATERIAL))
+ .build();
+
+ return materialFuture.thenApply(
+ material -> {
+ material.setFloat3(MATERIAL_COLOR, color);
+ applyDefaultPbrParams(material);
+ return material;
+ });
+ }
+
+ /**
+ * Creates a transparent {@link Material} with the {@link Color} passed in. The {@link Color} can
+ * be modified by calling {@link Material#setFloat4(String, Color)} with {@link #MATERIAL_COLOR}.
+ * The metallicness, roughness, and reflectance can be modified using {@link
+ * Material#setFloat(String, float)}.
+ *
+ * @see #MATERIAL_METALLIC
+ * @see #MATERIAL_ROUGHNESS
+ * @see #MATERIAL_REFLECTANCE
+ * @param context a context used for loading the material resource
+ * @param color the color for the material to render
+ * @return material that will render the given color
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ // CompletableFuture requires api level 24
+ public static CompletableFuture makeTransparentWithColor(Context context, Color color) {
+ CompletableFuture materialFuture =
+ Material.builder()
+ .setSource(
+ context,
+ RenderingResources.GetSceneformResource(
+ context, RenderingResources.Resource.TRANSPARENT_COLORED_MATERIAL))
+ .build();
+
+ return materialFuture.thenApply(
+ material -> {
+ material.setFloat4(MATERIAL_COLOR, color);
+ applyDefaultPbrParams(material);
+ return material;
+ });
+ }
+
+ /**
+ * Creates an opaque {@link Material} with the {@link Texture} passed in. The {@link Texture} can
+ * be modified by calling {@link Material#setTexture(String, Texture)} with {@link
+ * #MATERIAL_TEXTURE}. The metallicness, roughness, and reflectance can be modified using {@link
+ * Material#setFloat(String, float)}.
+ *
+ * @see #MATERIAL_METALLIC
+ * @see #MATERIAL_ROUGHNESS
+ * @see #MATERIAL_REFLECTANCE
+ * @param context a context used for loading the material resource
+ * @param texture the texture for the material to render
+ * @return material that will render the given texture
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ // CompletableFuture requires api level 24
+ public static CompletableFuture makeOpaqueWithTexture(
+ Context context, Texture texture) {
+ CompletableFuture materialFuture =
+ Material.builder()
+ .setSource(
+ context,
+ RenderingResources.GetSceneformResource(
+ context, RenderingResources.Resource.OPAQUE_TEXTURED_MATERIAL))
+ .build();
+
+ return materialFuture.thenApply(
+ material -> {
+ material.setTexture(MATERIAL_TEXTURE, texture);
+ applyDefaultPbrParams(material);
+ return material;
+ });
+ }
+
+ /**
+ * Creates a transparent {@link Material} with the {@link Texture} passed in. The {@link Texture}
+ * can be modified by calling {@link Material#setTexture(String, Texture)} with {@link
+ * #MATERIAL_TEXTURE}. The metallicness, roughness, and reflectance can be modified using {@link
+ * Material#setFloat(String, float)}.
+ *
+ * @see #MATERIAL_METALLIC
+ * @see #MATERIAL_ROUGHNESS
+ * @see #MATERIAL_REFLECTANCE
+ * @param context a context used for loading the material resource
+ * @param texture the texture for the material to render
+ * @return material that will render the given texture
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ // CompletableFuture requires api level 24
+ public static CompletableFuture makeTransparentWithTexture(
+ Context context, Texture texture) {
+ CompletableFuture materialFuture =
+ Material.builder()
+ .setSource(
+ context,
+ RenderingResources.GetSceneformResource(
+ context, RenderingResources.Resource.TRANSPARENT_TEXTURED_MATERIAL))
+ .build();
+
+ return materialFuture.thenApply(
+ material -> {
+ material.setTexture(MATERIAL_TEXTURE, texture);
+ applyDefaultPbrParams(material);
+ return material;
+ });
+ }
+
+ private static void applyDefaultPbrParams(Material material) {
+ material.setFloat(MATERIAL_METALLIC, DEFAULT_METALLIC_PROPERTY);
+ material.setFloat(MATERIAL_ROUGHNESS, DEFAULT_ROUGHNESS_PROPERTY);
+ material.setFloat(MATERIAL_REFLECTANCE, DEFAULT_REFLECTANCE_PROPERTY);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialInternalData.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialInternalData.java
new file mode 100644
index 0000000..b5d5ddc
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialInternalData.java
@@ -0,0 +1,7 @@
+package com.google.ar.sceneform.rendering;
+
+import com.google.ar.sceneform.resources.SharedReference;
+
+abstract class MaterialInternalData extends SharedReference {
+ abstract com.google.android.filament.Material getFilamentMaterial();
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialInternalDataGltfImpl.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialInternalDataGltfImpl.java
new file mode 100644
index 0000000..58bb970
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialInternalDataGltfImpl.java
@@ -0,0 +1,25 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.Nullable;
+import com.google.android.filament.Material;
+
+public class MaterialInternalDataGltfImpl extends MaterialInternalData {
+ @Nullable private final com.google.android.filament.Material filamentMaterial;
+
+ MaterialInternalDataGltfImpl(com.google.android.filament.Material filamentMaterial) {
+ this.filamentMaterial = filamentMaterial;
+ }
+
+ @Override
+ Material getFilamentMaterial() {
+ if (filamentMaterial == null) {
+ throw new IllegalStateException("Filament Material is null.");
+ }
+ return filamentMaterial;
+ }
+
+ @Override
+ protected void onDispose() {
+ // Resource tracked in the native-gltf loader.
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialInternalDataImpl.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialInternalDataImpl.java
new file mode 100644
index 0000000..ee8ad8d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialInternalDataImpl.java
@@ -0,0 +1,36 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.Nullable;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+
+/**
+ * Represents shared data used by {@link Material}s for rendering. The data will be released when
+ * all {@link Material}s using this data are finalized.
+ */
+class MaterialInternalDataImpl extends MaterialInternalData {
+ @Nullable private com.google.android.filament.Material filamentMaterial;
+
+ MaterialInternalDataImpl(com.google.android.filament.Material filamentMaterial) {
+ this.filamentMaterial = filamentMaterial;
+ }
+
+ @Override
+ com.google.android.filament.Material getFilamentMaterial() {
+ if (filamentMaterial == null) {
+ throw new IllegalStateException("Filament Material is null.");
+ }
+ return filamentMaterial;
+ }
+
+ @Override
+ protected void onDispose() {
+ AndroidPreconditions.checkUiThread();
+
+ IEngine engine = EngineInstance.getEngine();
+ com.google.android.filament.Material material = this.filamentMaterial;
+ this.filamentMaterial = null;
+ if (material != null && engine != null && engine.isValid()) {
+ engine.destroyMaterial(material);
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialParameters.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialParameters.java
new file mode 100644
index 0000000..39aef90
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/MaterialParameters.java
@@ -0,0 +1,610 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.Nullable;
+import com.google.android.filament.MaterialInstance;
+import com.google.android.filament.TextureSampler;
+
+import com.google.ar.core.annotations.UsedByNative;
+import com.google.ar.sceneform.math.Vector3;
+import java.util.HashMap;
+
+/** Material property store. */
+@UsedByNative("material_java_wrappers.h")
+final class MaterialParameters {
+ private final HashMap namedParameters = new HashMap<>();
+
+
+
+
+
+
+ @UsedByNative("material_java_wrappers.h")
+ void setBoolean(String name, boolean x) {
+ namedParameters.put(name, new MaterialParameters.BooleanParameter(name, x));
+ }
+
+ boolean getBoolean(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof BooleanParameter) {
+ BooleanParameter booleanParam = (BooleanParameter) param;
+ return booleanParam.x;
+ }
+
+ return false;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setBoolean2(String name, boolean x, boolean y) {
+ namedParameters.put(name, new MaterialParameters.Boolean2Parameter(name, x, y));
+ }
+
+ @Nullable
+ boolean[] getBoolean2(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof Boolean2Parameter) {
+ Boolean2Parameter boolean2 = (Boolean2Parameter) param;
+ return new boolean[] {boolean2.x, boolean2.y};
+ }
+
+ return null;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setBoolean3(String name, boolean x, boolean y, boolean z) {
+ namedParameters.put(name, new MaterialParameters.Boolean3Parameter(name, x, y, z));
+ }
+
+ @Nullable
+ boolean[] getBoolean3(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof Boolean3Parameter) {
+ Boolean3Parameter boolean3 = (Boolean3Parameter) param;
+ return new boolean[] {boolean3.x, boolean3.y, boolean3.z};
+ }
+
+ return null;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setBoolean4(String name, boolean x, boolean y, boolean z, boolean w) {
+ namedParameters.put(name, new MaterialParameters.Boolean4Parameter(name, x, y, z, w));
+ }
+
+ @Nullable
+ boolean[] getBoolean4(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof Boolean4Parameter) {
+ Boolean4Parameter boolean4 = (Boolean4Parameter) param;
+ return new boolean[] {boolean4.x, boolean4.y, boolean4.z, boolean4.w};
+ }
+
+ return null;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setFloat(String name, float x) {
+ namedParameters.put(name, new MaterialParameters.FloatParameter(name, x));
+ }
+
+ float getFloat(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof FloatParameter) {
+ FloatParameter floatParam = (FloatParameter) param;
+ return floatParam.x;
+ }
+
+ return 0.0f;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setFloat2(String name, float x, float y) {
+ namedParameters.put(name, new MaterialParameters.Float2Parameter(name, x, y));
+ }
+
+ @Nullable
+ float[] getFloat2(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof Float2Parameter) {
+ Float2Parameter float2 = (Float2Parameter) param;
+ return new float[] {float2.x, float2.y};
+ }
+
+ return null;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setFloat3(String name, float x, float y, float z) {
+ namedParameters.put(name, new MaterialParameters.Float3Parameter(name, x, y, z));
+ }
+
+ void setFloat3(String name, Vector3 value) {
+ namedParameters.put(
+ name, new MaterialParameters.Float3Parameter(name, value.x, value.y, value.z));
+ }
+
+ @Nullable
+ float[] getFloat3(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof Float3Parameter) {
+ Float3Parameter float3 = (Float3Parameter) param;
+ return new float[] {float3.x, float3.y, float3.z};
+ }
+
+ return null;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setFloat4(String name, float x, float y, float z, float w) {
+ namedParameters.put(name, new MaterialParameters.Float4Parameter(name, x, y, z, w));
+ }
+
+ @Nullable
+ float[] getFloat4(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof Float4Parameter) {
+ Float4Parameter float4 = (Float4Parameter) param;
+ return new float[] {float4.x, float4.y, float4.z, float4.w};
+ }
+
+ return null;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setInt(String name, int x) {
+ namedParameters.put(name, new MaterialParameters.IntParameter(name, x));
+ }
+
+ int getInt(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof IntParameter) {
+ IntParameter intParam = (IntParameter) param;
+ return intParam.x;
+ }
+
+ return 0;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setInt2(String name, int x, int y) {
+ namedParameters.put(name, new MaterialParameters.Int2Parameter(name, x, y));
+ }
+
+ @Nullable
+ int[] getInt2(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof Int2Parameter) {
+ Int2Parameter int2 = (Int2Parameter) param;
+ return new int[] {int2.x, int2.y};
+ }
+
+ return null;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setInt3(String name, int x, int y, int z) {
+ namedParameters.put(name, new MaterialParameters.Int3Parameter(name, x, y, z));
+ }
+
+ @Nullable
+ int[] getInt3(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof Int3Parameter) {
+ Int3Parameter int3 = (Int3Parameter) param;
+ return new int[] {int3.x, int3.y, int3.z};
+ }
+
+ return null;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setInt4(String name, int x, int y, int z, int w) {
+ namedParameters.put(name, new MaterialParameters.Int4Parameter(name, x, y, z, w));
+ }
+
+ @Nullable
+ int[] getInt4(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof Int4Parameter) {
+ Int4Parameter int4 = (Int4Parameter) param;
+ return new int[] {int4.x, int4.y, int4.z, int4.w};
+ }
+
+ return null;
+ }
+
+ @UsedByNative("material_java_wrappers.h")
+ void setTexture(String name, Texture texture) {
+ namedParameters.put(name, new MaterialParameters.TextureParameter(name, texture));
+ }
+
+ @Nullable
+ Texture getTexture(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof TextureParameter) {
+ return ((TextureParameter) param).texture;
+ }
+
+ return null;
+ }
+
+ void setExternalTexture(String name, ExternalTexture externalTexture) {
+ namedParameters.put(
+ name, new MaterialParameters.ExternalTextureParameter(name, externalTexture));
+ }
+
+ @Nullable
+ ExternalTexture getExternalTexture(String name) {
+ Parameter param = namedParameters.get(name);
+ if (param instanceof ExternalTextureParameter) {
+ return ((ExternalTextureParameter) param).externalTexture;
+ }
+
+ return null;
+ }
+
+ void applyTo(MaterialInstance materialInstance) {
+ com.google.android.filament.Material material = materialInstance.getMaterial();
+
+ for (MaterialParameters.Parameter value : namedParameters.values()) {
+ if (material.hasParameter(value.name)) {
+ value.applyTo(materialInstance);
+ }
+ }
+ }
+
+ void copyFrom(MaterialParameters other) {
+ namedParameters.clear();
+ merge(other);
+ }
+
+ void merge(MaterialParameters other) {
+ for (MaterialParameters.Parameter value : other.namedParameters.values()) {
+ MaterialParameters.Parameter clonedValue = value.clone();
+ namedParameters.put(clonedValue.name, clonedValue);
+ }
+ }
+
+ void mergeIfAbsent(MaterialParameters other) {
+ for (MaterialParameters.Parameter value : other.namedParameters.values()) {
+ if (!namedParameters.containsKey(value.name)) {
+ MaterialParameters.Parameter clonedValue = value.clone();
+ namedParameters.put(clonedValue.name, clonedValue);
+ }
+ }
+ }
+
+ abstract static class Parameter implements Cloneable {
+ String name;
+
+ abstract void applyTo(MaterialInstance materialInstance);
+
+ @Override
+ public MaterialParameters.Parameter clone() {
+ try {
+ return (MaterialParameters.Parameter) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ static class BooleanParameter extends MaterialParameters.Parameter {
+ boolean x;
+
+ BooleanParameter(String name, boolean x) {
+ this.name = name;
+ this.x = x;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x);
+ }
+ }
+
+ static class Boolean2Parameter extends MaterialParameters.Parameter {
+ boolean x;
+ boolean y;
+
+ Boolean2Parameter(String name, boolean x, boolean y) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x, y);
+ }
+ }
+
+ static class Boolean3Parameter extends MaterialParameters.Parameter {
+ boolean x;
+ boolean y;
+ boolean z;
+
+ Boolean3Parameter(String name, boolean x, boolean y, boolean z) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x, y, z);
+ }
+ }
+
+ static class Boolean4Parameter extends MaterialParameters.Parameter {
+ boolean x;
+ boolean y;
+ boolean z;
+ boolean w;
+
+ Boolean4Parameter(String name, boolean x, boolean y, boolean z, boolean w) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.w = w;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x, y, z, w);
+ }
+ }
+
+ static class FloatParameter extends MaterialParameters.Parameter {
+ float x;
+
+ FloatParameter(String name, float x) {
+ this.name = name;
+ this.x = x;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x);
+ }
+ }
+
+ static class Float2Parameter extends MaterialParameters.Parameter {
+ float x;
+ float y;
+
+ Float2Parameter(String name, float x, float y) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x, y);
+ }
+ }
+
+ static class Float3Parameter extends MaterialParameters.Parameter {
+ float x;
+ float y;
+ float z;
+
+ Float3Parameter(String name, float x, float y, float z) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x, y, z);
+ }
+ }
+
+ static class Float4Parameter extends MaterialParameters.Parameter {
+ float x;
+ float y;
+ float z;
+ float w;
+
+ Float4Parameter(String name, float x, float y, float z, float w) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.w = w;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x, y, z, w);
+ }
+ }
+
+ static class IntParameter extends MaterialParameters.Parameter {
+ int x;
+
+ IntParameter(String name, int x) {
+ this.name = name;
+ this.x = x;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x);
+ }
+ }
+
+ static class Int2Parameter extends MaterialParameters.Parameter {
+ int x;
+ int y;
+
+ Int2Parameter(String name, int x, int y) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x, y);
+ }
+ }
+
+ static class Int3Parameter extends MaterialParameters.Parameter {
+ int x;
+ int y;
+ int z;
+
+ Int3Parameter(String name, int x, int y, int z) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x, y, z);
+ }
+ }
+
+ static class Int4Parameter extends MaterialParameters.Parameter {
+ int x;
+ int y;
+ int z;
+ int w;
+
+ Int4Parameter(String name, int x, int y, int z, int w) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.w = w;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(name, x, y, z, w);
+ }
+ }
+
+ static class TextureParameter extends MaterialParameters.Parameter {
+ final Texture texture;
+
+ TextureParameter(String name, Texture texture) {
+ this.name = name;
+ this.texture = texture;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ materialInstance.setParameter(
+ name, texture.getFilamentTexture(), convertTextureSampler(texture.getSampler()));
+ }
+
+ @Override
+ public MaterialParameters.Parameter clone() {
+ return new MaterialParameters.TextureParameter(name, texture);
+ }
+ }
+
+ static class ExternalTextureParameter extends MaterialParameters.Parameter {
+ private final ExternalTexture externalTexture;
+
+ ExternalTextureParameter(String name, ExternalTexture externalTexture) {
+ this.name = name;
+ this.externalTexture = externalTexture;
+ }
+
+ @Override
+ void applyTo(MaterialInstance materialInstance) {
+ com.google.android.filament.TextureSampler filamentSampler = getExternalFilamentSampler();
+
+ materialInstance.setParameter(name, externalTexture.getFilamentTexture(), filamentSampler);
+ }
+
+ private com.google.android.filament.TextureSampler getExternalFilamentSampler() {
+ com.google.android.filament.TextureSampler filamentSampler =
+ new com.google.android.filament.TextureSampler();
+ filamentSampler.setMinFilter(TextureSampler.MinFilter.LINEAR);
+ filamentSampler.setMagFilter(TextureSampler.MagFilter.LINEAR);
+ filamentSampler.setWrapModeS(TextureSampler.WrapMode.CLAMP_TO_EDGE);
+ filamentSampler.setWrapModeT(TextureSampler.WrapMode.CLAMP_TO_EDGE);
+ filamentSampler.setWrapModeR(TextureSampler.WrapMode.CLAMP_TO_EDGE);
+ return filamentSampler;
+ }
+
+ @Override
+ public MaterialParameters.Parameter clone() {
+ return new ExternalTextureParameter(name, externalTexture);
+ }
+ }
+
+ private static com.google.android.filament.TextureSampler convertTextureSampler(
+ Texture.Sampler sampler) {
+ com.google.android.filament.TextureSampler convertedSampler =
+ new com.google.android.filament.TextureSampler();
+
+ switch (sampler.getMinFilter()) {
+ case NEAREST:
+ convertedSampler.setMinFilter(com.google.android.filament.TextureSampler.MinFilter.NEAREST);
+ break;
+ case LINEAR:
+ convertedSampler.setMinFilter(com.google.android.filament.TextureSampler.MinFilter.LINEAR);
+ break;
+ case NEAREST_MIPMAP_NEAREST:
+ convertedSampler.setMinFilter(
+ com.google.android.filament.TextureSampler.MinFilter.NEAREST_MIPMAP_NEAREST);
+ break;
+ case LINEAR_MIPMAP_NEAREST:
+ convertedSampler.setMinFilter(
+ com.google.android.filament.TextureSampler.MinFilter.LINEAR_MIPMAP_NEAREST);
+ break;
+ case NEAREST_MIPMAP_LINEAR:
+ convertedSampler.setMinFilter(
+ com.google.android.filament.TextureSampler.MinFilter.NEAREST_MIPMAP_LINEAR);
+ break;
+ case LINEAR_MIPMAP_LINEAR:
+ convertedSampler.setMinFilter(
+ com.google.android.filament.TextureSampler.MinFilter.LINEAR_MIPMAP_LINEAR);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid MinFilter");
+ }
+
+ switch (sampler.getMagFilter()) {
+ case NEAREST:
+ convertedSampler.setMagFilter(com.google.android.filament.TextureSampler.MagFilter.NEAREST);
+ break;
+ case LINEAR:
+ convertedSampler.setMagFilter(com.google.android.filament.TextureSampler.MagFilter.LINEAR);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid MagFilter");
+ }
+
+ convertedSampler.setWrapModeS(convertWrapMode(sampler.getWrapModeS()));
+ convertedSampler.setWrapModeT(convertWrapMode(sampler.getWrapModeT()));
+ convertedSampler.setWrapModeR(convertWrapMode(sampler.getWrapModeR()));
+
+ return convertedSampler;
+ }
+
+ private static com.google.android.filament.TextureSampler.WrapMode convertWrapMode(
+ Texture.Sampler.WrapMode wrapMode) {
+ switch (wrapMode) {
+ case CLAMP_TO_EDGE:
+ return com.google.android.filament.TextureSampler.WrapMode.CLAMP_TO_EDGE;
+ case REPEAT:
+ return com.google.android.filament.TextureSampler.WrapMode.REPEAT;
+ case MIRRORED_REPEAT:
+ return com.google.android.filament.TextureSampler.WrapMode.MIRRORED_REPEAT;
+ default:
+ throw new IllegalArgumentException("Invalid WrapMode");
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ModelRenderable.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ModelRenderable.java
new file mode 100644
index 0000000..efa6938
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ModelRenderable.java
@@ -0,0 +1,206 @@
+package com.google.ar.sceneform.rendering;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.google.ar.sceneform.resources.ResourceRegistry;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+
+
+/**
+ * Renders a 3D Model by attaching it to a {@link com.google.ar.sceneform.Node} with {@link
+ * com.google.ar.sceneform.Node#setRenderable(Renderable)}.
+ *
+ * {@code
+ * future = ModelRenderable.builder().setSource(context, R.raw.renderable).build();
+ * renderable = future.thenAccept(...);
+ * }
+ */
+@RequiresApi(api = Build.VERSION_CODES.N)
+public class ModelRenderable extends Renderable {
+
+
+
+
+
+
+
+
+ private ModelRenderable(Builder builder) {
+ super(builder);
+ }
+
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ private ModelRenderable(ModelRenderable other) {
+ super(other);
+
+ copyAnimationFrom(other);
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private void copyAnimationFrom(ModelRenderable other) {return ;}
+
+
+
+
+
+
+
+
+ /**
+ * Creates a new instance of this ModelRenderable.
+ *
+ * The new renderable will have unique copy of all mutable state. All materials referenced by
+ * the ModelRenderable will also be instanced. Immutable data will be shared between the
+ * instances.
+ */
+ @Override
+ public ModelRenderable makeCopy() {
+ return new ModelRenderable(this);
+ }
+
+ /** Constructs a {@link ModelRenderable}. */
+ public static Builder builder() {
+ AndroidPreconditions.checkMinAndroidApiLevel();
+ return new Builder();
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /** Factory class for {@link ModelRenderable}. */
+ public static final class Builder extends Renderable.Builder {
+
+ /** @hide */
+ @Override
+ protected ModelRenderable makeRenderable() {
+ return new ModelRenderable(this);
+ }
+
+ /** @hide */
+ @Override
+ protected Class getRenderableClass() {
+ return ModelRenderable.class;
+ }
+
+ /** @hide */
+ @Override
+ protected ResourceRegistry getRenderableRegistry() {
+ return ResourceManager.getInstance().getModelRenderableRegistry();
+ }
+
+ /** @hide */
+ @Override
+ protected Builder getSelf() {
+ return this;
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/PlaneRenderer.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/PlaneRenderer.java
new file mode 100644
index 0000000..6bf5afe
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/PlaneRenderer.java
@@ -0,0 +1,326 @@
+package com.google.ar.sceneform.rendering;
+
+import android.util.Log;
+import androidx.annotation.Nullable;
+import com.google.ar.core.Frame;
+import com.google.ar.core.HitResult;
+import com.google.ar.core.Plane;
+import com.google.ar.core.Pose;
+import com.google.ar.core.Trackable;
+import com.google.ar.core.TrackingState;
+
+import com.google.ar.sceneform.math.Vector3;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Control rendering of ARCore planes.
+ *
+ * Used to visualize detected planes and to control whether Renderables cast shadows on them.
+ */
+@SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"}) // CompletableFuture
+public class PlaneRenderer {
+ private static final String TAG = PlaneRenderer.class.getSimpleName();
+
+ /** Material parameter that controls what texture is being used when rendering the planes. */
+ public static final String MATERIAL_TEXTURE = "texture";
+
+ /**
+ * Float2 material parameter to control the X/Y scaling of the texture's UV coordinates. Can be
+ * used to adjust for the texture's aspect ratio and control the frequency of tiling.
+ */
+ public static final String MATERIAL_UV_SCALE = "uvScale";
+
+ /** Float3 material parameter to control the RGB tint of the plane. */
+ public static final String MATERIAL_COLOR = "color";
+
+ /** Float material parameter to control the radius of the spotlight. */
+ public static final String MATERIAL_SPOTLIGHT_RADIUS = "radius";
+
+ /** Float3 material parameter to control the grid visualization point. */
+ private static final String MATERIAL_SPOTLIGHT_FOCUS_POINT = "focusPoint";
+
+ /** Used to control the UV Scale for the default texture. */
+ private static final float BASE_UV_SCALE = 8.0f;
+
+ private static final float DEFAULT_TEXTURE_WIDTH = 293;
+ private static final float DEFAULT_TEXTURE_HEIGHT = 513;
+
+ private static final float SPOTLIGHT_RADIUS = .5f;
+
+ private final Renderer renderer;
+
+ private final Map visualizerMap = new HashMap<>();
+ private CompletableFuture planeMaterialFuture;
+
+ private Material shadowMaterial;
+
+ private boolean isEnabled = true;
+ private boolean isVisible = true;
+ private boolean isShadowReceiver = true;
+
+ // Per-plane overrides
+ private final Map materialOverrides = new HashMap<>();
+
+ // Distance from the camera to last plane hit, default value is 4 meters (standing height).
+ private float lastPlaneHitDistance = 4.0f;
+
+ /** Enable/disable the plane renderer. */
+ public void setEnabled(boolean enabled) {
+ if (isEnabled != enabled) {
+ isEnabled = enabled;
+
+ for (PlaneVisualizer visualizer : visualizerMap.values()) {
+ visualizer.setEnabled(isEnabled);
+ }
+ }
+ }
+
+ /** Check if the plane renderer is enabled. */
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ /**
+ * Control whether Renderables in the scene should cast shadows onto the planes.
+ *
+ * If false - no planes receive shadows, regardless of the per-plane setting.
+ */
+ public void setShadowReceiver(boolean shadowReceiver) {
+ if (isShadowReceiver != shadowReceiver) {
+ isShadowReceiver = shadowReceiver;
+
+ for (PlaneVisualizer visualizer : visualizerMap.values()) {
+ visualizer.setShadowReceiver(isShadowReceiver);
+ }
+ }
+ }
+
+ /** Return true if Renderables in the scene cast shadows onto the planes. */
+ public boolean isShadowReceiver() {
+ return isShadowReceiver;
+ }
+
+ /**
+ * Control visibility of plane visualization.
+ *
+ *
If false - no planes are drawn. Note that shadow visibility is independent of plane
+ * visibility.
+ */
+ public void setVisible(boolean visible) {
+ if (isVisible != visible) {
+ isVisible = visible;
+
+ for (PlaneVisualizer visualizer : visualizerMap.values()) {
+ visualizer.setVisible(isVisible);
+ }
+ }
+ }
+
+ /** Return true if plane visualization is visible. */
+ public boolean isVisible() {
+ return isVisible;
+ }
+
+ /** Returns default material instance used to render the planes. */
+ public CompletableFuture getMaterial() {
+ return planeMaterialFuture;
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /** @hide PlaneRenderer is constructed in a different package, but not part of external API. */
+ @SuppressWarnings("initialization")
+ public PlaneRenderer(Renderer renderer) {
+ this.renderer = renderer;
+
+ loadPlaneMaterial();
+ loadShadowMaterial();
+ }
+
+ /** @hide PlaneRenderer is updated in a different package, but not part of external API. */
+ public void update(Frame frame, int viewWidth, int viewHeight) {
+ Collection updatedPlanes = frame.getUpdatedTrackables(Plane.class);
+ Vector3 focusPoint = getFocusPoint(frame, viewWidth, viewHeight);
+
+ @SuppressWarnings("nullness")
+ @Nullable
+ Material planeMaterial = planeMaterialFuture.getNow(null);
+ if (planeMaterial != null) {
+ planeMaterial.setFloat3(MATERIAL_SPOTLIGHT_FOCUS_POINT, focusPoint);
+ planeMaterial.setFloat(MATERIAL_SPOTLIGHT_RADIUS, SPOTLIGHT_RADIUS);
+ }
+
+ for (Plane plane : updatedPlanes) {
+ PlaneVisualizer planeVisualizer;
+
+ // Find the plane visualizer if it already exists.
+ // If not, create a new plane visualizer for this plane.
+ if (visualizerMap.containsKey(plane)) {
+ planeVisualizer = visualizerMap.get(plane);
+ } else {
+ planeVisualizer = new PlaneVisualizer(plane, renderer);
+ Material overrideMaterial = materialOverrides.get(plane);
+ if (overrideMaterial != null) {
+ planeVisualizer.setPlaneMaterial(overrideMaterial);
+ } else if (planeMaterial != null) {
+ planeVisualizer.setPlaneMaterial(planeMaterial);
+ }
+ if (shadowMaterial != null) {
+ planeVisualizer.setShadowMaterial(shadowMaterial);
+ }
+ planeVisualizer.setShadowReceiver(isShadowReceiver);
+ planeVisualizer.setVisible(isVisible);
+ planeVisualizer.setEnabled(isEnabled);
+ visualizerMap.put(plane, planeVisualizer);
+ }
+
+ // Update the plane visualizer.
+ planeVisualizer.updatePlane();
+ }
+
+ // Remove plane visualizers for old planes that are no longer tracking.
+ // Update the material parameters for all remaining planes.
+ Iterator> iter = visualizerMap.entrySet().iterator();
+ while (iter.hasNext()) {
+ Map.Entry entry = iter.next();
+ Plane plane = entry.getKey();
+ PlaneVisualizer planeVisualizer = entry.getValue();
+
+ // If this plane was subsumed by another plane or it has permanently stopped tracking,
+ // remove it.
+ if (plane.getSubsumedBy() != null || plane.getTrackingState() == TrackingState.STOPPED) {
+ planeVisualizer.release();
+ iter.remove();
+ continue;
+ }
+ }
+ }
+
+ @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
+ private void loadShadowMaterial() {
+ Material.builder()
+ .setSource(
+ renderer.getContext(),
+ RenderingResources.GetSceneformResource(
+ renderer.getContext(), RenderingResources.Resource.PLANE_SHADOW_MATERIAL))
+ .build()
+ .thenAccept(
+ material -> {
+ shadowMaterial = material;
+ for (PlaneVisualizer visualizer : visualizerMap.values()) {
+ visualizer.setShadowMaterial(shadowMaterial);
+ }
+ })
+ .exceptionally(
+ throwable -> {
+ Log.e(TAG, "Unable to load plane shadow material.", throwable);
+ return null;
+ });
+ }
+
+ @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
+ private void loadPlaneMaterial() {
+ Texture.Sampler sampler =
+ Texture.Sampler.builder()
+ .setMinMagFilter(Texture.Sampler.MagFilter.LINEAR)
+ .setWrapMode(Texture.Sampler.WrapMode.REPEAT)
+ .build();
+
+ CompletableFuture textureFuture =
+ Texture.builder()
+ .setSource(
+ renderer.getContext(),
+ RenderingResources.GetSceneformResource(
+ renderer.getContext(), RenderingResources.Resource.PLANE))
+ .setSampler(sampler)
+ .build();
+
+ planeMaterialFuture =
+ Material.builder()
+ .setSource(
+ renderer.getContext(),
+ RenderingResources.GetSceneformResource(
+ renderer.getContext(), RenderingResources.Resource.PLANE_MATERIAL))
+ .build()
+ .thenCombine(
+ textureFuture,
+ (material, texture) -> {
+ material.setTexture(MATERIAL_TEXTURE, texture);
+ material.setFloat3(MATERIAL_COLOR, 1.0f, 1.0f, 1.0f);
+
+ // TODO: Don't use hardcoded width and height... Need api for getting
+ // width and
+ // height from the Texture class.
+ float widthToHeightRatio = DEFAULT_TEXTURE_WIDTH / DEFAULT_TEXTURE_HEIGHT;
+ float scaleX = BASE_UV_SCALE;
+ float scaleY = scaleX * widthToHeightRatio;
+ material.setFloat2(MATERIAL_UV_SCALE, scaleX, scaleY);
+
+ for (Map.Entry entry : visualizerMap.entrySet()) {
+ if (!materialOverrides.containsKey(entry.getKey())) {
+ entry.getValue().setPlaneMaterial(material);
+ }
+ }
+ return material;
+ });
+ }
+
+ private Vector3 getFocusPoint(Frame frame, int width, int height) {
+ Vector3 focusPoint;
+
+ // If we hit a plane, return the hit point.
+ List hits = frame.hitTest(width / 2, height / 2);
+ if (hits != null && !hits.isEmpty()) {
+ for (HitResult hit : hits) {
+ Trackable trackable = hit.getTrackable();
+ Pose hitPose = hit.getHitPose();
+ if (trackable instanceof Plane && ((Plane) trackable).isPoseInPolygon(hitPose)) {
+ focusPoint = new Vector3(hitPose.tx(), hitPose.ty(), hitPose.tz());
+ lastPlaneHitDistance = hit.getDistance();
+ return focusPoint;
+ }
+ }
+ }
+
+ // If we didn't hit anything, project a point in front of the camera so that the spotlight
+ // rolls off the edge smoothly.
+ Pose cameraPose = frame.getCamera().getPose();
+ Vector3 cameraPosition = new Vector3(cameraPose.tx(), cameraPose.ty(), cameraPose.tz());
+ float[] zAxis = cameraPose.getZAxis();
+ Vector3 backwards = new Vector3(zAxis[0], zAxis[1], zAxis[2]);
+
+ focusPoint = Vector3.add(cameraPosition, backwards.scaled(-lastPlaneHitDistance));
+
+ return focusPoint;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/PlaneVisualizer.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/PlaneVisualizer.java
new file mode 100644
index 0000000..37f5781
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/PlaneVisualizer.java
@@ -0,0 +1,280 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.Nullable;
+import com.google.ar.core.Plane;
+import com.google.ar.core.TrackingState;
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.RenderableDefinition.Submesh;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/** Renders a single ARCore Plane. */
+class PlaneVisualizer implements TransformProvider {
+ private static final String TAG = PlaneVisualizer.class.getSimpleName();
+
+ private final Plane plane;
+ private final Renderer renderer;
+
+ private final Matrix planeMatrix = new Matrix();
+
+ private boolean isPlaneAddedToScene = false;
+ private boolean isEnabled = false;
+ private boolean isShadowReceiver = false;
+ private boolean isVisible = false;
+
+ @Nullable private ModelRenderable planeRenderable = null;
+ @Nullable private RenderableInstance planeRenderableInstance;
+
+ private final ArrayList vertices = new ArrayList<>();
+ private final ArrayList triangleIndices = new ArrayList<>();
+ private final RenderableDefinition renderableDefinition;
+ @Nullable private Submesh planeSubmesh;
+ @Nullable private Submesh shadowSubmesh;
+
+ private static final int VERTS_PER_BOUNDARY_VERT = 2;
+
+ // Feather distance 0.2 meters.
+ private static final float FEATHER_LENGTH = 0.2f;
+
+ // Feather scale over the distance between plane center and vertices.
+ private static final float FEATHER_SCALE = 0.2f;
+
+ public void setEnabled(boolean enabled) {
+ if (isEnabled != enabled) {
+ isEnabled = enabled;
+ updatePlane();
+ }
+ }
+
+ public void setShadowReceiver(boolean shadowReceiver) {
+ if (isShadowReceiver != shadowReceiver) {
+ isShadowReceiver = shadowReceiver;
+ updatePlane();
+ }
+ }
+
+ public void setVisible(boolean visible) {
+ if (isVisible != visible) {
+ isVisible = visible;
+ updatePlane();
+ }
+ }
+
+ PlaneVisualizer(Plane plane, Renderer renderer) {
+ this.plane = plane;
+ this.renderer = renderer;
+
+ renderableDefinition = RenderableDefinition.builder().setVertices(vertices).build();
+ }
+
+ Plane getPlane() {
+ return plane;
+ }
+
+ void setShadowMaterial(Material material) {
+ if (shadowSubmesh == null) {
+ shadowSubmesh =
+ Submesh.builder().setTriangleIndices(triangleIndices).setMaterial(material).build();
+ } else {
+ shadowSubmesh.setMaterial(material);
+ }
+
+ if (planeRenderable != null) {
+ updateRenderable();
+ }
+ }
+
+ void setPlaneMaterial(Material material) {
+ if (planeSubmesh == null) {
+ planeSubmesh =
+ Submesh.builder().setTriangleIndices(triangleIndices).setMaterial(material).build();
+ } else {
+ planeSubmesh.setMaterial(material);
+ }
+
+ if (planeRenderable != null) {
+ updateRenderable();
+ }
+ }
+
+ @Override
+ public Matrix getWorldModelMatrix() {
+ return planeMatrix;
+ }
+
+ void updatePlane() {
+ if (!isEnabled || (!isVisible && !isShadowReceiver)) {
+ removePlaneFromScene();
+ return;
+ }
+
+ if (plane.getTrackingState() != TrackingState.TRACKING) {
+ removePlaneFromScene();
+ return;
+ }
+
+ // Set the transformation matrix to the pose of the plane.
+ plane.getCenterPose().toMatrix(planeMatrix.data, 0);
+
+ // Calculate the mesh for the plane.
+ boolean success = updateRenderableDefinitionForPlane();
+ if (!success) {
+ removePlaneFromScene();
+ return;
+ }
+
+ updateRenderable();
+ addPlaneToScene();
+ }
+
+ @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
+ void updateRenderable() {
+ List submeshes = renderableDefinition.getSubmeshes();
+ submeshes.clear();
+
+ // the order of the meshes is important here, because we set the blendOrder based on
+ // the index below.
+ if (isVisible && (planeSubmesh != null)) {
+ submeshes.add(planeSubmesh);
+ }
+
+ if (isShadowReceiver && (shadowSubmesh != null)) {
+ submeshes.add(shadowSubmesh);
+ }
+
+ if (submeshes.isEmpty()) {
+ removePlaneFromScene();
+ return;
+ }
+
+ if (planeRenderable == null) {
+ try {
+ planeRenderable = ModelRenderable.builder().setSource(renderableDefinition).build().get();
+ planeRenderable.setShadowCaster(false);
+ // Creating a Renderable is immediate when using RenderableDefinition.
+ } catch (InterruptedException | ExecutionException ex) {
+ throw new AssertionError("Unable to create plane renderable.");
+ }
+ planeRenderableInstance = planeRenderable.createInstance(this);
+ } else {
+ planeRenderable.updateFromDefinition(renderableDefinition);
+ }
+
+ // The plane must always be drawn before the shadow, we use the blendOrder to enforce that.
+ // this works because both sub-meshes will be sorted at the same distance from the camera
+ // since they're part of the same renderable. The blendOrder, determines the sorting in
+ // that situation.
+ if (planeRenderableInstance != null && submeshes.size() > 1) {
+ planeRenderableInstance.setBlendOrderAt(0, 0); // plane
+ planeRenderableInstance.setBlendOrderAt(1, 1); // shadow
+ }
+ }
+
+ void release() {
+ removePlaneFromScene();
+
+ planeRenderable = null;
+ }
+
+ private void addPlaneToScene() {
+ if (isPlaneAddedToScene || planeRenderableInstance == null) {
+ return;
+ }
+
+ renderer.addInstance(planeRenderableInstance);
+ isPlaneAddedToScene = true;
+ }
+
+ private void removePlaneFromScene() {
+ if (!isPlaneAddedToScene || planeRenderableInstance == null) {
+ return;
+ }
+
+ renderer.removeInstance(planeRenderableInstance);
+ isPlaneAddedToScene = false;
+ }
+
+ private boolean updateRenderableDefinitionForPlane() {
+ FloatBuffer boundary = plane.getPolygon();
+
+ if (boundary == null) {
+ return false;
+ }
+
+ boundary.rewind();
+ int boundaryVertices = boundary.limit() / 2;
+
+ if (boundaryVertices == 0) {
+ return false;
+ }
+
+ int numVertices = boundaryVertices * VERTS_PER_BOUNDARY_VERT;
+ vertices.clear();
+ vertices.ensureCapacity(numVertices);
+
+ int numIndices = (boundaryVertices * 6) + ((boundaryVertices - 2) * 3);
+ triangleIndices.clear();
+ triangleIndices.ensureCapacity(numIndices);
+
+ Vector3 normal = Vector3.up();
+
+ // Copy the perimeter vertices into the vertex buffer and add in the y-coordinate.
+ while (boundary.hasRemaining()) {
+ float x = boundary.get();
+ float z = boundary.get();
+ vertices.add(Vertex.builder().setPosition(new Vector3(x, 0.0f, z)).setNormal(normal).build());
+ }
+
+ // Generate the interior vertices.
+ boundary.rewind();
+ while (boundary.hasRemaining()) {
+ float x = boundary.get();
+ float z = boundary.get();
+
+ float magnitude = (float) Math.hypot(x, z);
+ float scale = 1.0f - FEATHER_SCALE;
+ if (magnitude != 0.0f) {
+ scale = 1.0f - Math.min(FEATHER_LENGTH / magnitude, FEATHER_SCALE);
+ }
+
+ vertices.add(
+ Vertex.builder()
+ .setPosition(new Vector3(x * scale, 1.0f, z * scale))
+ .setNormal(normal)
+ .build());
+ }
+
+ int firstOuterVertex = 0;
+ int firstInnerVertex = (short) boundaryVertices;
+
+ // Generate triangle (4, 5, 6) and (4, 6, 7).
+ for (int i = 0; i < boundaryVertices - 2; ++i) {
+ triangleIndices.add(firstInnerVertex);
+ triangleIndices.add(firstInnerVertex + i + 1);
+ triangleIndices.add(firstInnerVertex + i + 2);
+ }
+
+ // Generate triangle (0, 1, 4), (4, 1, 5), (5, 1, 2), (5, 2, 6), (6, 2, 3), (6, 3, 7)
+ // (7, 3, 0), (7, 0, 4)
+ for (int i = 0; i < boundaryVertices; ++i) {
+ int outerVertex1 = firstOuterVertex + i;
+ int outerVertex2 = firstOuterVertex + ((i + 1) % boundaryVertices);
+ int innerVertex1 = firstInnerVertex + i;
+ int innerVertex2 = firstInnerVertex + ((i + 1) % boundaryVertices);
+
+ triangleIndices.add(outerVertex1);
+ triangleIndices.add(outerVertex2);
+ triangleIndices.add(innerVertex1);
+
+ triangleIndices.add(innerVertex1);
+ triangleIndices.add(outerVertex2);
+ triangleIndices.add(innerVertex2);
+ }
+
+ return true;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderViewToExternalTexture.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderViewToExternalTexture.java
new file mode 100644
index 0000000..eeb18a7
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderViewToExternalTexture.java
@@ -0,0 +1,157 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Picture;
+import android.graphics.PorterDuff;
+import android.view.Surface;
+import android.view.View;
+import android.widget.LinearLayout;
+import androidx.annotation.Nullable;
+
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.util.ArrayList;
+
+/**
+ * Used to render an android view to a native open GL texture that can then be rendered by open GL.
+ *
+ * To correctly draw a hardware accelerated animated view to a surface texture, the view MUST be
+ * attached to a window and drawn to a real DisplayListCanvas, which is a hidden class. To achieve
+ * this, the following is done:
+ *
+ *
+ * - Attach RenderViewToSurfaceTexture to the WindowManager.
+ *
- Override dispatchDraw.
+ *
- Call super.dispatchDraw with the real DisplayListCanvas
+ *
- Draw the clear color the DisplayListCanvas so that it isn't visible on screen.
+ *
- Draw the view to the SurfaceTexture every frame. This must be done every frame, because the
+ * view will not be marked as dirty when child views are animating when hardware accelerated.
+ *
+ *
+ * @hide
+ */
+
+class RenderViewToExternalTexture extends LinearLayout {
+ /** Interface definition for a callback to be invoked when the size of the view changes. */
+ public interface OnViewSizeChangedListener {
+ void onViewSizeChanged(int width, int height);
+ }
+
+ private final View view;
+ private final ExternalTexture externalTexture;
+ private final Picture picture = new Picture();
+ private boolean hasDrawnToSurfaceTexture = false;
+
+ @Nullable private ViewAttachmentManager viewAttachmentManager;
+ private final ArrayList onViewSizeChangedListeners = new ArrayList<>();
+
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ RenderViewToExternalTexture(Context context, View view) {
+ super(context);
+ Preconditions.checkNotNull(view, "Parameter \"view\" was null.");
+
+ externalTexture = new ExternalTexture();
+
+ this.view = view;
+ addView(view);
+ }
+
+ /**
+ * Register a callback to be invoked when the size of the view changes.
+ *
+ * @param onViewSizeChangedListener the listener to attach
+ */
+ void addOnViewSizeChangedListener(OnViewSizeChangedListener onViewSizeChangedListener) {
+ if (!onViewSizeChangedListeners.contains(onViewSizeChangedListener)) {
+ onViewSizeChangedListeners.add(onViewSizeChangedListener);
+ }
+ }
+
+ /**
+ * Remove a callback to be invoked when the size of the view changes.
+ *
+ * @param onViewSizeChangedListener the listener to remove
+ */
+ void removeOnViewSizeChangedListener(OnViewSizeChangedListener onViewSizeChangedListener) {
+ onViewSizeChangedListeners.remove(onViewSizeChangedListener);
+ }
+
+ ExternalTexture getExternalTexture() {
+ return externalTexture;
+ }
+
+ boolean hasDrawnToSurfaceTexture() {
+ return hasDrawnToSurfaceTexture;
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ externalTexture.getSurfaceTexture().setDefaultBufferSize(view.getWidth(), view.getHeight());
+ }
+
+ @Override
+ public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ for (OnViewSizeChangedListener onViewSizeChangedListener : onViewSizeChangedListeners) {
+ onViewSizeChangedListener.onViewSizeChanged(width, height);
+ }
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ // Sanity that the surface is valid.
+ Surface targetSurface = externalTexture.getSurface();
+ if (!targetSurface.isValid()) {
+ return;
+ }
+
+ if (view.isDirty()) {
+ Canvas pictureCanvas = picture.beginRecording(view.getWidth(), view.getHeight());
+ pictureCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
+ super.dispatchDraw(pictureCanvas);
+ picture.endRecording();
+
+ Canvas surfaceCanvas = targetSurface.lockCanvas(null);
+ picture.draw(surfaceCanvas);
+ targetSurface.unlockCanvasAndPost(surfaceCanvas);
+
+ hasDrawnToSurfaceTexture = true;
+ }
+
+ invalidate();
+ }
+
+ void attachView(ViewAttachmentManager viewAttachmentManager) {
+ if (this.viewAttachmentManager != null) {
+ if (this.viewAttachmentManager != viewAttachmentManager) {
+ throw new IllegalStateException(
+ "Cannot use the same ViewRenderable with multiple SceneViews.");
+ }
+
+ return;
+ }
+
+ this.viewAttachmentManager = viewAttachmentManager;
+ viewAttachmentManager.addView(this);
+ }
+
+ void detachView() {
+ if (viewAttachmentManager != null) {
+ viewAttachmentManager.removeView(this);
+ viewAttachmentManager = null;
+ }
+ }
+
+ void releaseResources() {
+ detachView();
+
+ // Let Surface and SurfaceTexture be released
+ // automatically by their finalizers.
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Renderable.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Renderable.java
new file mode 100644
index 0000000..fb620dc
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Renderable.java
@@ -0,0 +1,524 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import android.net.Uri;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.ar.sceneform.collision.Box;
+import com.google.ar.sceneform.collision.CollisionShape;
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.resources.ResourceRegistry;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.ChangeId;
+import com.google.ar.sceneform.utilities.LoadHelper;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+/**
+ * Base class for rendering in 3D space by attaching to a {@link com.google.ar.sceneform.Node} with
+ * {@link com.google.ar.sceneform.Node#setRenderable(Renderable)}.
+ */
+@SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"}) // CompletableFuture
+public abstract class Renderable {
+ // Data that can be shared between Renderables with makeCopy()
+ private final IRenderableInternalData renderableData;
+
+ // Data that is unique per-Renderable.
+ private final ArrayList materialBindings = new ArrayList<>();
+ private final ArrayList materialNames = new ArrayList<>();
+ private int renderPriority = RENDER_PRIORITY_DEFAULT;
+ private boolean isShadowCaster = true;
+ private boolean isShadowReceiver = true;
+ @Nullable protected CollisionShape collisionShape;
+
+ private final ChangeId changeId = new ChangeId();
+
+ public static final int RENDER_PRIORITY_DEFAULT = 4;
+ public static final int RENDER_PRIORITY_FIRST = 0;
+ public static final int RENDER_PRIORITY_LAST = 7;
+ // Allow stale data two weeks old by default.
+ private static final long DEFAULT_MAX_STALE_CACHE = TimeUnit.DAYS.toSeconds(14);
+
+ /** @hide */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ protected Renderable(Renderable.Builder extends Renderable, ? extends Builder, ?>> builder) {
+ Preconditions.checkNotNull(builder, "Parameter \"builder\" was null.");
+ if (builder.isFilamentAsset) {
+ renderableData = new RenderableInternalFilamentAssetData();
+ } else if (builder.isGltf) {
+ renderableData = createRenderableInternalGltfData();
+ } else {
+ renderableData = new RenderableInternalData();
+ }
+ if (builder.definition != null) {
+ updateFromDefinition(builder.definition);
+ }
+ }
+
+ @SuppressWarnings("initialization")
+ protected Renderable(Renderable other) {
+ if (other.getId().isEmpty()) {
+ throw new AssertionError("Cannot copy uninitialized Renderable.");
+ }
+
+ // Share renderableData with the original Renderable.
+ renderableData = other.renderableData;
+
+ // Copy materials.
+ Preconditions.checkState(other.materialNames.size() == other.materialBindings.size());
+ for (int i = 0; i < other.materialBindings.size(); i++) {
+ Material otherMaterial = other.materialBindings.get(i);
+ materialBindings.add(otherMaterial.makeCopy());
+ materialNames.add(other.materialNames.get(i));
+ }
+
+ renderPriority = other.renderPriority;
+ isShadowCaster = other.isShadowCaster;
+ isShadowReceiver = other.isShadowReceiver;
+
+ // Copy collision shape.
+ if (other.collisionShape != null) {
+ collisionShape = other.collisionShape.makeCopy();
+ }
+
+ changeId.update();
+ }
+
+ /** Get the {@link CollisionShape} used for collision detection with this {@link Renderable}. */
+ public @Nullable CollisionShape getCollisionShape() {
+ return collisionShape;
+ }
+
+ /** Set the {@link CollisionShape} used for collision detection with this {@link Renderable}. */
+ public void setCollisionShape(@Nullable CollisionShape collisionShape) {
+ this.collisionShape = collisionShape;
+ changeId.update();
+ }
+
+ /** Returns the material bound to the first submesh. */
+ public Material getMaterial() {
+ return getMaterial(0);
+ }
+
+ /** Returns the material bound to the specified submesh. */
+ public Material getMaterial(int submeshIndex) {
+ if (submeshIndex < materialBindings.size()) {
+ return materialBindings.get(submeshIndex);
+ }
+
+ throw makeSubmeshOutOfRangeException(submeshIndex);
+ }
+
+ /** Sets the material bound to the first submesh. */
+ public void setMaterial(Material material) {
+ setMaterial(0, material);
+ }
+
+ /** Sets the material bound to the specified submesh. */
+ public void setMaterial(int submeshIndex, Material material) {
+ if (submeshIndex < materialBindings.size()) {
+ materialBindings.set(submeshIndex, material);
+ changeId.update();
+ } else {
+ throw makeSubmeshOutOfRangeException(submeshIndex);
+ }
+ }
+
+ /**
+ * Returns the name associated with the specified submesh.
+ *
+ * @throws IllegalArgumentException if the index is out of range
+ */
+ public String getSubmeshName(int submeshIndex) {
+ Preconditions.checkState(materialNames.size() == materialBindings.size());
+ if (submeshIndex >= 0 && submeshIndex < materialNames.size()) {
+ return materialNames.get(submeshIndex);
+ }
+
+ throw makeSubmeshOutOfRangeException(submeshIndex);
+ }
+
+ /**
+ * Get the render priority that controls the order of rendering. The priority is between a range
+ * of 0 (rendered first) and 7 (rendered last). The default value is 4.
+ */
+ public int getRenderPriority() {
+ return renderPriority;
+ }
+
+ /**
+ * Set the render priority to control the order of rendering. The priority is between a range of 0
+ * (rendered first) and 7 (rendered last). The default value is 4.
+ */
+ public void setRenderPriority(
+ @IntRange(from = RENDER_PRIORITY_FIRST, to = RENDER_PRIORITY_LAST) int renderPriority) {
+ this.renderPriority =
+ Math.min(RENDER_PRIORITY_LAST, Math.max(RENDER_PRIORITY_FIRST, renderPriority));
+ changeId.update();
+ }
+
+ /** Returns true if configured to cast shadows on other renderables. */
+ public boolean isShadowCaster() {
+ return isShadowCaster;
+ }
+
+ /** Sets whether the renderable casts shadow on other renderables in the scene. */
+ public void setShadowCaster(boolean isShadowCaster) {
+ this.isShadowCaster = isShadowCaster;
+ changeId.update();
+ }
+
+ /** Returns true if configured to receive shadows cast by other renderables. */
+ public boolean isShadowReceiver() {
+ return isShadowReceiver;
+ }
+
+ /** Sets whether the renderable receives shadows cast by other renderables in the scene. */
+ public void setShadowReceiver(boolean isShadowReceiver) {
+ this.isShadowReceiver = isShadowReceiver;
+ changeId.update();
+ }
+
+ /**
+ * Returns the number of submeshes that this renderable has. All Renderables have at least one.
+ */
+ public int getSubmeshCount() {
+ return renderableData.getMeshes().size();
+ }
+
+ /** @hide */
+ public ChangeId getId() {
+ return changeId;
+ }
+
+ /** @hide */
+ public RenderableInstance createInstance(TransformProvider transformProvider) {
+ return new RenderableInstance(transformProvider, this);
+ }
+
+ public void updateFromDefinition(RenderableDefinition definition) {
+ Preconditions.checkState(!definition.getSubmeshes().isEmpty());
+
+ changeId.update();
+
+ definition.applyDefinitionToData(renderableData, materialBindings, materialNames);
+
+ collisionShape = new Box(renderableData.getSizeAabb(), renderableData.getCenterAabb());
+ }
+
+ /**
+ * Creates a new instance of this Renderable.
+ *
+ * The new renderable will have unique copy of all mutable state. All materials referenced by
+ * the Renderable will also be instanced. Immutable data will be shared between the instances.
+ */
+ public abstract Renderable makeCopy();
+
+ IRenderableInternalData getRenderableData() {
+ return renderableData;
+ }
+
+ ArrayList getMaterialBindings() {
+ return materialBindings;
+ }
+
+ ArrayList getMaterialNames() {
+ return materialNames;
+ }
+
+ /**
+ * Optionally override in subclasses for work that must be done each frame for specific types of
+ * Renderables. For example, ViewRenderable uses this to prevent the renderable from being visible
+ * until the view has been successfully drawn to an external texture, and initializing material
+ * parameters.
+ */
+ void prepareForDraw() {}
+
+ void attachToRenderer(Renderer renderer) {}
+
+ void detatchFromRenderer() {}
+
+ /**
+ * Gets the final model matrix to use for rendering this {@link Renderable} based on the matrix
+ * passed in. Default implementation simply passes through the original matrix. WARNING: Do not
+ * modify the originalMatrix! If the final matrix isn't the same as the original matrix, then a
+ * new instance must be returned.
+ *
+ * @hide
+ */
+ public Matrix getFinalModelMatrix(final Matrix originalMatrix) {
+ Preconditions.checkNotNull(originalMatrix, "Parameter \"originalMatrix\" was null.");
+ return originalMatrix;
+ }
+
+ private IllegalArgumentException makeSubmeshOutOfRangeException(int submeshIndex) {
+ return new IllegalArgumentException(
+ "submeshIndex ("
+ + submeshIndex
+ + ") is out of range. It must be less than the submeshCount ("
+ + getSubmeshCount()
+ + ").");
+ }
+
+
+ private IRenderableInternalData createRenderableInternalGltfData() {return null;}
+
+
+
+ // TODO: Gltf animation api should be consistent with Sceneform.
+
+
+
+
+
+ // TODO: Gltf animation api should be consistent with Sceneform.
+
+
+
+
+
+
+ /**
+ * Used to programmatically construct a {@link Renderable}. Builder data is stored, not copied. Be
+ * careful when modifying the data before or between build calls.
+ */
+ @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"}) // CompletableFuture
+ abstract static class Builder> {
+ /** @hide */
+ @Nullable protected Object registryId = null;
+ /** @hide */
+ @Nullable protected Context context = null;
+
+ @Nullable private Uri sourceUri = null;
+ @Nullable private Callable inputStreamCreator = null;
+ @Nullable private RenderableDefinition definition = null;
+ private boolean isGltf = false;
+ private boolean isFilamentAsset = false;
+ @Nullable private LoadGltfListener loadGltfListener;
+ @Nullable private Function uriResolver = null;
+ @Nullable private byte[] materialsBytes = null;
+
+ /** Used to programmatically construct a {@link Renderable}. */
+ protected Builder() {}
+
+ public B setSource(Context context, Callable inputStreamCreator) {
+ Preconditions.checkNotNull(inputStreamCreator);
+ this.sourceUri = null;
+ this.inputStreamCreator = inputStreamCreator;
+ this.context = context;
+ return getSelf();
+ }
+
+ public B setSource(Context context, Uri sourceUri) {
+ return setRemoteSourceHelper(context, sourceUri, true);
+ }
+
+
+ public B setSource(Context context, Uri sourceUri, boolean enableCaching) {return null;}
+
+
+
+ public B setSource(Context context, int resource) {
+ this.inputStreamCreator = LoadHelper.fromResource(context, resource);
+ this.context = context;
+
+ Uri uri = LoadHelper.resourceToUri(context, resource);
+ this.sourceUri = uri;
+ this.registryId = uri;
+ return getSelf();
+ }
+
+ /** Build a {@link Renderable} from a {@link RenderableDefinition}. */
+ public B setSource(RenderableDefinition definition) {
+ this.definition = definition;
+ registryId = null;
+ sourceUri = null;
+ return getSelf();
+ }
+
+ public B setRegistryId(@Nullable Object registryId) {
+ this.registryId = registryId;
+ return getSelf();
+ }
+
+
+
+
+
+
+
+
+
+ public B setIsFilamentGltf(boolean isFilamentGltf) {
+ this.isFilamentAsset = isFilamentGltf;
+ return getSelf();
+ }
+
+
+
+
+
+
+
+ /**
+ * True if a source function will be called during build
+ *
+ * @hide
+ */
+ public Boolean hasSource() {
+ return sourceUri != null || inputStreamCreator != null || definition != null;
+ }
+
+ /**
+ * Constructs a {@link Renderable} with the parameters of the builder.
+ *
+ * @return the constructed {@link Renderable}
+ */
+ public CompletableFuture build() {
+ try {
+ checkPreconditions();
+ } catch (Throwable failedPrecondition) {
+ CompletableFuture result = new CompletableFuture<>();
+ result.completeExceptionally(failedPrecondition);
+ FutureHelper.logOnException(
+ getRenderableClass().getSimpleName(),
+ result,
+ "Unable to load Renderable registryId='" + registryId + "'");
+ return result;
+ }
+
+ // For static-analysis check.
+ Object registryId = this.registryId;
+ if (registryId != null) {
+ // See if a renderable has already been registered by this id, if so re-use it.
+ ResourceRegistry registry = getRenderableRegistry();
+ CompletableFuture renderableFuture = registry.get(registryId);
+ if (renderableFuture != null) {
+ return renderableFuture.thenApply(
+ renderable -> getRenderableClass().cast(renderable.makeCopy()));
+ }
+ }
+
+ T renderable = makeRenderable();
+
+ if (definition != null) {
+ return CompletableFuture.completedFuture(renderable);
+ }
+
+ // For static-analysis check.
+ Callable inputStreamCreator = this.inputStreamCreator;
+ if (inputStreamCreator == null) {
+ CompletableFuture result = new CompletableFuture<>();
+ result.completeExceptionally(new AssertionError("Input Stream Creator is null."));
+ FutureHelper.logOnException(
+ getRenderableClass().getSimpleName(),
+ result,
+ "Unable to load Renderable registryId='" + registryId + "'");
+ return result;
+ }
+
+ CompletableFuture result = null;
+ if (isFilamentAsset) {
+ if (context != null) {
+ result = loadRenderableFromFilamentGltf(context, renderable);
+ } else {
+ throw new AssertionError("Gltf Renderable.Builder must have a valid context.");
+ }
+ } else if (isGltf) {
+ if (context != null) {
+ result = loadRenderableFromGltf(context, renderable, this.materialsBytes);
+ } else {
+ throw new AssertionError("Gltf Renderable.Builder must have a valid context.");
+ }
+ } else {
+ LoadRenderableFromSfbTask loader =
+ new LoadRenderableFromSfbTask<>(renderable, sourceUri);
+ result = loader.downloadAndProcessRenderable(inputStreamCreator);
+ }
+
+ if (registryId != null) {
+ ResourceRegistry registry = getRenderableRegistry();
+ registry.register(registryId, result);
+ }
+
+ FutureHelper.logOnException(
+ getRenderableClass().getSimpleName(),
+ result,
+ "Unable to load Renderable registryId='" + registryId + "'");
+ return result.thenApply(
+ resultRenderable -> getRenderableClass().cast(resultRenderable.makeCopy()));
+ }
+
+ protected void checkPreconditions() {
+ AndroidPreconditions.checkUiThread();
+
+ if (!hasSource()) {
+ throw new AssertionError("ModelRenderable must have a source.");
+ }
+ }
+
+ private B setRemoteSourceHelper(Context context, Uri sourceUri, boolean enableCaching) {
+ Preconditions.checkNotNull(sourceUri);
+ this.sourceUri = sourceUri;
+ this.context = context;
+ this.registryId = sourceUri;
+ // Configure caching.
+ if (enableCaching) {
+ this.setCachingEnabled(context);
+ }
+
+ Map connectionProperties = new HashMap<>();
+ if (!enableCaching) {
+ connectionProperties.put("Cache-Control", "no-cache");
+ } else {
+ connectionProperties.put("Cache-Control", "max-stale=" + DEFAULT_MAX_STALE_CACHE);
+ }
+ this.inputStreamCreator =
+ LoadHelper.fromUri(
+ context, Preconditions.checkNotNull(this.sourceUri), connectionProperties);
+ return getSelf();
+ }
+
+
+ private CompletableFuture loadRenderableFromGltf(
+ @NonNull Context context, T renderable, @Nullable byte[] materialsBytes) {return null;}
+
+
+
+
+
+
+
+ private CompletableFuture loadRenderableFromFilamentGltf(
+ @NonNull Context context, T renderable) {
+ LoadRenderableFromFilamentGltfTask loader =
+ new LoadRenderableFromFilamentGltfTask<>(
+ renderable, context, Preconditions.checkNotNull(sourceUri), uriResolver);
+ return loader.downloadAndProcessRenderable(Preconditions.checkNotNull(inputStreamCreator));
+ }
+
+
+ private void setCachingEnabled(Context context) {return ;}
+
+
+
+ protected abstract T makeRenderable();
+
+ protected abstract Class getRenderableClass();
+
+ protected abstract ResourceRegistry getRenderableRegistry();
+
+ protected abstract B getSelf();
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableDefinition.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableDefinition.java
new file mode 100644
index 0000000..26fbb65
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableDefinition.java
@@ -0,0 +1,536 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.Nullable;
+import com.google.android.filament.IndexBuffer;
+import com.google.android.filament.IndexBuffer.Builder.IndexType;
+import com.google.android.filament.VertexBuffer;
+import com.google.android.filament.VertexBuffer.VertexAttribute;
+import com.google.ar.sceneform.math.MathHelper;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Quaternion;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.Vertex.UvCoordinate;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * Represents the visual information of a {@link Renderable}. Can be used to construct and modify
+ * renderables dynamically.
+ *
+ * @see ModelRenderable.Builder
+ * @see ViewRenderable.Builder
+ */
+public class RenderableDefinition {
+ private static final Matrix scratchMatrix = new Matrix();
+
+ /**
+ * Represents a Submesh for a RenderableDefinition. Each RenderableDefinition may have multiple
+ * Submeshes.
+ */
+ public static class Submesh {
+ private List triangleIndices;
+ private Material material;
+ @Nullable private String name;
+
+ public void setTriangleIndices(List triangleIndices) {
+ this.triangleIndices = triangleIndices;
+ }
+
+ public List getTriangleIndices() {
+ return triangleIndices;
+ }
+
+ public void setMaterial(Material material) {
+ this.material = material;
+ }
+
+ public Material getMaterial() {
+ return material;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Nullable
+ public String getName() {
+ return name;
+ }
+
+ private Submesh(Builder builder) {
+ triangleIndices = Preconditions.checkNotNull(builder.triangleIndices);
+ material = Preconditions.checkNotNull(builder.material);
+ name = builder.name;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Factory class for {@link Submesh}. */
+ public static final class Builder {
+ @Nullable private List triangleIndices;
+ @Nullable private Material material;
+ @Nullable private String name;
+
+ public Builder setTriangleIndices(List triangleIndices) {
+ this.triangleIndices = triangleIndices;
+ return this;
+ }
+
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder setMaterial(Material material) {
+ this.material = material;
+ return this;
+ }
+
+ public Submesh build() {
+ return new Submesh(this);
+ }
+ }
+ }
+
+ private List vertices;
+ private List submeshes;
+
+ private static final int BYTES_PER_FLOAT = Float.SIZE / 8;
+ private static final int POSITION_SIZE = 3; // x, y, z
+ private static final int UV_SIZE = 2;
+ private static final int TANGENTS_SIZE = 4; // quaternion
+ private static final int COLOR_SIZE = 4; // RGBA
+
+ public void setVertices(List vertices) {
+ this.vertices = vertices;
+ }
+
+ List getVertices() {
+ return vertices;
+ }
+
+ public void setSubmeshes(List submeshes) {
+ this.submeshes = submeshes;
+ }
+
+ List getSubmeshes() {
+ return submeshes;
+ }
+
+ void applyDefinitionToData(
+ // TODO: Split into RenderableInternalSfbData & RenderableInternalDefinitionData
+ IRenderableInternalData data,
+ ArrayList materialBindings,
+ ArrayList materialNames) {
+ AndroidPreconditions.checkUiThread();
+
+ applyDefinitionToDataIndexBuffer(data);
+ applyDefinitionToDataVertexBuffer(data);
+
+ // Update/Add mesh data.
+ int indexStart = 0;
+ materialBindings.clear();
+ materialNames.clear();
+ for (int i = 0; i < submeshes.size(); i++) {
+ Submesh submesh = submeshes.get(i);
+
+ RenderableInternalData.MeshData meshData;
+ if (i < data.getMeshes().size()) {
+ meshData = data.getMeshes().get(i);
+ } else {
+ meshData = new RenderableInternalData.MeshData();
+ data.getMeshes().add(meshData);
+ }
+
+ meshData.indexStart = indexStart;
+ meshData.indexEnd = indexStart + submesh.getTriangleIndices().size();
+ indexStart = meshData.indexEnd;
+ materialBindings.add(submesh.getMaterial());
+ final String name = submesh.getName();
+ materialNames.add(name != null ? name : "");
+ }
+
+ // Remove old mesh data.
+ while (data.getMeshes().size() > submeshes.size()) {
+ data.getMeshes().remove(data.getMeshes().size() - 1);
+ }
+ }
+
+ private void applyDefinitionToDataIndexBuffer(IRenderableInternalData data) {
+ // Determine how many indices there are.
+ int numIndices = 0;
+ for (int i = 0; i < submeshes.size(); i++) {
+ Submesh submesh = submeshes.get(i);
+ numIndices += submesh.getTriangleIndices().size();
+ }
+
+ // Create the raw index buffer if needed.
+ IntBuffer rawIndexBuffer = data.getRawIndexBuffer();
+ if (rawIndexBuffer == null || rawIndexBuffer.capacity() < numIndices) {
+ rawIndexBuffer = IntBuffer.allocate(numIndices);
+ data.setRawIndexBuffer(rawIndexBuffer);
+ } else {
+ rawIndexBuffer.rewind();
+ }
+
+ // Fill the index buffer with the data.
+ for (int i = 0; i < submeshes.size(); i++) {
+ Submesh submesh = submeshes.get(i);
+ List triangleIndices = submesh.getTriangleIndices();
+ for (int j = 0; j < triangleIndices.size(); j++) {
+ rawIndexBuffer.put(triangleIndices.get(j));
+ }
+ }
+ rawIndexBuffer.rewind();
+
+ // Create the filament index buffer if needed.
+ IndexBuffer indexBuffer = data.getIndexBuffer();
+ IEngine engine = EngineInstance.getEngine();
+ if (indexBuffer == null || indexBuffer.getIndexCount() < numIndices) {
+ if (indexBuffer != null) {
+ engine.destroyIndexBuffer(indexBuffer);
+ }
+
+ indexBuffer =
+ new IndexBuffer.Builder()
+ .indexCount(numIndices)
+ .bufferType(IndexType.UINT)
+ .build(engine.getFilamentEngine());
+ data.setIndexBuffer(indexBuffer);
+ }
+
+ indexBuffer.setBuffer(engine.getFilamentEngine(), rawIndexBuffer, 0, numIndices);
+ }
+
+ private void applyDefinitionToDataVertexBuffer(IRenderableInternalData data) {
+ if (vertices.isEmpty()) {
+ throw new IllegalArgumentException("RenderableDescription must have at least one vertex.");
+ }
+
+ int numVertices = vertices.size();
+ Vertex firstVertex = vertices.get(0);
+
+ // Determine which attributes this VertexBuffer needs.
+ EnumSet descriptionAttributes = EnumSet.of(VertexAttribute.POSITION);
+ if (firstVertex.getNormal() != null) {
+ descriptionAttributes.add(VertexAttribute.TANGENTS);
+ }
+ if (firstVertex.getUvCoordinate() != null) {
+ descriptionAttributes.add(VertexAttribute.UV0);
+ }
+ if (firstVertex.getColor() != null) {
+ descriptionAttributes.add(VertexAttribute.COLOR);
+ }
+
+ // Determine if the filament vertex buffer needs to be re-created.
+ VertexBuffer vertexBuffer = data.getVertexBuffer();
+ boolean createVertexBuffer = true;
+ if (vertexBuffer != null) {
+ EnumSet oldAttributes = EnumSet.of(VertexAttribute.POSITION);
+ if (data.getRawTangentsBuffer() != null) {
+ oldAttributes.add(VertexAttribute.TANGENTS);
+ }
+ if (data.getRawUvBuffer() != null) {
+ oldAttributes.add(VertexAttribute.UV0);
+ }
+ if (data.getRawColorBuffer() != null) {
+ oldAttributes.add(VertexAttribute.COLOR);
+ }
+
+ createVertexBuffer =
+ !oldAttributes.equals(descriptionAttributes)
+ || vertexBuffer.getVertexCount() < numVertices;
+
+ if (createVertexBuffer) {
+ EngineInstance.getEngine().destroyVertexBuffer(vertexBuffer);
+ }
+ }
+
+ if (createVertexBuffer) {
+ vertexBuffer = createVertexBuffer(numVertices, descriptionAttributes);
+ data.setVertexBuffer(vertexBuffer);
+ }
+
+ // Create position Buffer if needed.
+ FloatBuffer positionBuffer = data.getRawPositionBuffer();
+ if (positionBuffer == null || positionBuffer.capacity() < numVertices * POSITION_SIZE) {
+ positionBuffer = FloatBuffer.allocate(numVertices * POSITION_SIZE);
+ data.setRawPositionBuffer(positionBuffer);
+ } else {
+ positionBuffer.rewind();
+ }
+
+ // Create tangents Buffer if needed.
+ FloatBuffer tangentsBuffer = data.getRawTangentsBuffer();
+ if (descriptionAttributes.contains(VertexAttribute.TANGENTS)
+ && (tangentsBuffer == null || tangentsBuffer.capacity() < numVertices * TANGENTS_SIZE)) {
+ tangentsBuffer = FloatBuffer.allocate(numVertices * TANGENTS_SIZE);
+ data.setRawTangentsBuffer(tangentsBuffer);
+ } else if (tangentsBuffer != null) {
+ tangentsBuffer.rewind();
+ }
+
+ // Create uv Buffer if needed.
+ FloatBuffer uvBuffer = data.getRawUvBuffer();
+ if (descriptionAttributes.contains(VertexAttribute.UV0)
+ && (uvBuffer == null || uvBuffer.capacity() < numVertices * UV_SIZE)) {
+ uvBuffer = FloatBuffer.allocate(numVertices * UV_SIZE);
+ data.setRawUvBuffer(uvBuffer);
+ } else if (uvBuffer != null) {
+ uvBuffer.rewind();
+ }
+
+ // Create color Buffer if needed.
+ FloatBuffer colorBuffer = data.getRawColorBuffer();
+ if (descriptionAttributes.contains(VertexAttribute.COLOR)
+ && (colorBuffer == null || colorBuffer.capacity() < numVertices * COLOR_SIZE)) {
+ colorBuffer = FloatBuffer.allocate(numVertices * COLOR_SIZE);
+ data.setRawColorBuffer(colorBuffer);
+ } else if (colorBuffer != null) {
+ colorBuffer.rewind();
+ }
+
+ // Variables for calculating the Aabb of the renderable.
+ Vector3 minAabb = new Vector3();
+ Vector3 maxAabb = new Vector3();
+ Vector3 firstPosition = firstVertex.getPosition();
+ minAabb.set(firstPosition);
+ maxAabb.set(firstPosition);
+
+ // Update the raw buffers and calculate the Aabb in one pass through the vertices.
+ for (int i = 0; i < vertices.size(); i++) {
+ Vertex vertex = vertices.get(i);
+
+ // Aabb.
+ Vector3 position = vertex.getPosition();
+ minAabb.set(Vector3.min(minAabb, position));
+ maxAabb.set(Vector3.max(maxAabb, position));
+
+ // Position attribute.
+ addVector3ToBuffer(position, positionBuffer);
+
+ // Tangents attribute.
+ if (tangentsBuffer != null) {
+ Vector3 normal = vertex.getNormal();
+ if (normal == null) {
+ throw new IllegalArgumentException(
+ "Missing normal: If any Vertex in a "
+ + "RenderableDescription has a normal, all vertices must have one.");
+ }
+
+ Quaternion tangent = normalToTangent(normal);
+ addQuaternionToBuffer(tangent, tangentsBuffer);
+ }
+
+ // Uv attribute.
+ if (uvBuffer != null) {
+ UvCoordinate uvCoordinate = vertex.getUvCoordinate();
+ if (uvCoordinate == null) {
+ throw new IllegalArgumentException(
+ "Missing UV Coordinate: If any Vertex in a "
+ + "RenderableDescription has a UV Coordinate, all vertices must have one.");
+ }
+
+ addUvToBuffer(uvCoordinate, uvBuffer);
+ }
+
+ // Color attribute.
+ if (colorBuffer != null) {
+ Color color = vertex.getColor();
+ if (color == null) {
+ throw new IllegalArgumentException(
+ "Missing Color: If any Vertex in a "
+ + "RenderableDescription has a Color, all vertices must have one.");
+ }
+
+ addColorToBuffer(color, colorBuffer);
+ }
+ }
+
+ // Set the Aabb in the renderable data.
+ Vector3 extentsAabb = Vector3.subtract(maxAabb, minAabb).scaled(0.5f);
+ Vector3 centerAabb = Vector3.add(minAabb, extentsAabb);
+ data.setExtentsAabb(extentsAabb);
+ data.setCenterAabb(centerAabb);
+
+ if (vertexBuffer == null) {
+ throw new AssertionError("VertexBuffer is null.");
+ }
+
+ IEngine engine = EngineInstance.getEngine();
+ positionBuffer.rewind();
+ int bufferIndex = 0;
+ vertexBuffer.setBufferAt(
+ engine.getFilamentEngine(), bufferIndex, positionBuffer, 0, numVertices * POSITION_SIZE);
+
+ if (tangentsBuffer != null) {
+ tangentsBuffer.rewind();
+ bufferIndex++;
+ vertexBuffer.setBufferAt(
+ engine.getFilamentEngine(), bufferIndex, tangentsBuffer, 0, numVertices * TANGENTS_SIZE);
+ }
+
+ if (uvBuffer != null) {
+ uvBuffer.rewind();
+ bufferIndex++;
+ vertexBuffer.setBufferAt(
+ engine.getFilamentEngine(), bufferIndex, uvBuffer, 0, numVertices * UV_SIZE);
+ }
+
+ if (colorBuffer != null) {
+ colorBuffer.rewind();
+ bufferIndex++;
+ vertexBuffer.setBufferAt(
+ engine.getFilamentEngine(), bufferIndex, colorBuffer, 0, numVertices * COLOR_SIZE);
+ }
+ }
+
+ private RenderableDefinition(Builder builder) {
+ vertices = Preconditions.checkNotNull(builder.vertices);
+ submeshes = Preconditions.checkNotNull(builder.submeshes);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private static VertexBuffer createVertexBuffer(
+ int vertexCount, EnumSet attributes) {
+ VertexBuffer.Builder builder = new VertexBuffer.Builder();
+
+ builder.vertexCount(vertexCount).bufferCount(attributes.size());
+
+ // Position Attribute.
+ int bufferIndex = 0;
+ builder.attribute(
+ VertexBuffer.VertexAttribute.POSITION,
+ bufferIndex,
+ VertexBuffer.AttributeType.FLOAT3,
+ 0,
+ POSITION_SIZE * BYTES_PER_FLOAT);
+
+ // Tangents Attribute.
+ if (attributes.contains(VertexAttribute.TANGENTS)) {
+ bufferIndex++;
+ builder.attribute(
+ VertexBuffer.VertexAttribute.TANGENTS,
+ bufferIndex,
+ VertexBuffer.AttributeType.FLOAT4,
+ 0,
+ TANGENTS_SIZE * BYTES_PER_FLOAT);
+ }
+
+ // Uv Attribute.
+ if (attributes.contains(VertexAttribute.UV0)) {
+ bufferIndex++;
+ builder.attribute(
+ VertexBuffer.VertexAttribute.UV0,
+ bufferIndex,
+ VertexBuffer.AttributeType.FLOAT2,
+ 0,
+ UV_SIZE * BYTES_PER_FLOAT);
+ }
+
+ // Color Attribute.
+ if (attributes.contains(VertexAttribute.COLOR)) {
+ bufferIndex++;
+ builder.attribute(
+ VertexAttribute.COLOR,
+ bufferIndex,
+ VertexBuffer.AttributeType.FLOAT4,
+ 0,
+ COLOR_SIZE * BYTES_PER_FLOAT);
+ }
+
+ return builder.build(EngineInstance.getEngine().getFilamentEngine());
+ }
+
+ private static void addVector3ToBuffer(Vector3 vector3, FloatBuffer buffer) {
+ buffer.put(vector3.x);
+ buffer.put(vector3.y);
+ buffer.put(vector3.z);
+ }
+
+ private static void addUvToBuffer(Vertex.UvCoordinate uvCoordinate, FloatBuffer buffer) {
+ buffer.put(uvCoordinate.x);
+ buffer.put(uvCoordinate.y);
+ }
+
+ private static void addQuaternionToBuffer(Quaternion quaternion, FloatBuffer buffer) {
+ buffer.put(quaternion.x);
+ buffer.put(quaternion.y);
+ buffer.put(quaternion.z);
+ buffer.put(quaternion.w);
+ }
+
+ private static void addColorToBuffer(Color color, FloatBuffer buffer) {
+ buffer.put(color.r);
+ buffer.put(color.g);
+ buffer.put(color.b);
+ buffer.put(color.a);
+ }
+
+ private static Quaternion normalToTangent(Vector3 normal) {
+ Vector3 tangent;
+ Vector3 bitangent;
+
+ // Calculate basis vectors (+x = tangent, +y = bitangent, +z = normal).
+ tangent = Vector3.cross(Vector3.up(), normal);
+
+ // Uses almostEqualRelativeAndAbs for equality checks that account for float inaccuracy.
+ if (MathHelper.almostEqualRelativeAndAbs(Vector3.dot(tangent, tangent), 0.0f)) {
+ bitangent = Vector3.cross(normal, Vector3.right()).normalized();
+ tangent = Vector3.cross(bitangent, normal).normalized();
+ } else {
+ tangent.set(tangent.normalized());
+ bitangent = Vector3.cross(normal, tangent).normalized();
+ }
+
+ // Rotation of a 4x4 Transformation Matrix is represented by the top-left 3x3 elements.
+ final int rowOne = 0;
+ scratchMatrix.data[rowOne] = tangent.x;
+ scratchMatrix.data[rowOne + 1] = tangent.y;
+ scratchMatrix.data[rowOne + 2] = tangent.z;
+
+ final int rowTwo = 4;
+ scratchMatrix.data[rowTwo] = bitangent.x;
+ scratchMatrix.data[rowTwo + 1] = bitangent.y;
+ scratchMatrix.data[rowTwo + 2] = bitangent.z;
+
+ final int rowThree = 8;
+ scratchMatrix.data[rowThree] = normal.x;
+ scratchMatrix.data[rowThree + 1] = normal.y;
+ scratchMatrix.data[rowThree + 2] = normal.z;
+
+ Quaternion orientationQuaternion = new Quaternion();
+ scratchMatrix.extractQuaternion(orientationQuaternion);
+ return orientationQuaternion;
+ }
+
+ /** Factory class for {@link RenderableDefinition}. */
+ public static final class Builder {
+ @Nullable private List vertices;
+ @Nullable private List submeshes = new ArrayList<>();
+
+ public Builder setVertices(List vertices) {
+ this.vertices = vertices;
+ return this;
+ }
+
+ public Builder setSubmeshes(List submeshes) {
+ this.submeshes = submeshes;
+ return this;
+ }
+
+ public RenderableDefinition build() {
+ return new RenderableDefinition(this);
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableInstance.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableInstance.java
new file mode 100644
index 0000000..418c7bf
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableInstance.java
@@ -0,0 +1,467 @@
+package com.google.ar.sceneform.rendering;
+
+import android.net.Uri;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import androidx.annotation.Size;
+import com.google.android.filament.Engine;
+import com.google.android.filament.Entity;
+import com.google.android.filament.EntityInstance;
+import com.google.android.filament.EntityManager;
+
+import com.google.android.filament.RenderableManager;
+import com.google.android.filament.TransformManager;
+import com.google.android.filament.gltfio.AssetLoader;
+import com.google.android.filament.gltfio.FilamentAsset;
+import com.google.android.filament.gltfio.ResourceLoader;
+
+
+import com.google.ar.sceneform.collision.Box;
+import com.google.ar.sceneform.common.TransformProvider;
+import com.google.ar.sceneform.math.Matrix;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.ChangeId;
+import com.google.ar.sceneform.utilities.LoadHelper;
+import com.google.ar.sceneform.utilities.Preconditions;
+import com.google.ar.sceneform.utilities.SceneformBufferUtils;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+
+import java.nio.IntBuffer;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+
+/**
+ * Controls how a {@link Renderable} is displayed. There can be multiple RenderableInstances
+ * displaying a single Renderable.
+ *
+ * @hide
+ */
+@SuppressWarnings("AndroidJdkLibsChecker")
+public class RenderableInstance {
+
+ /**
+ * Interface for modifying the bone transforms for this specific RenderableInstance. Used by
+ * {@link com.google.ar.sceneform.SkeletonNode} to make it possible to control a bone by moving a
+ * node.
+ */
+ public interface SkinningModifier {
+
+ /**
+ * Takes the original boneTransforms and output new boneTransforms used to render the mesh.
+ *
+ * @param originalBuffer contains the bone transforms from the current animation state of the
+ * skeleton, buffer is read only
+ */
+ FloatBuffer modifyMaterialBoneTransformsBuffer(FloatBuffer originalBuffer);
+
+ boolean isModifiedSinceLastRender();
+ }
+
+ private static final String TAG = RenderableInstance.class.getSimpleName();
+
+ private final TransformProvider transformProvider;
+ private final Renderable renderable;
+ @Nullable private Renderer attachedRenderer;
+ @Entity private int entity = 0;
+ @Entity private int childEntity = 0;
+ int renderableId = ChangeId.EMPTY_ID;
+
+
+
+
+
+
+ @Nullable
+ FilamentAsset filamentAsset;
+
+ @Nullable private SkinningModifier skinningModifier;
+
+ @Nullable private Matrix cachedRelativeTransform;
+ @Nullable private Matrix cachedRelativeTransformInverse;
+
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ public RenderableInstance(TransformProvider transformProvider, Renderable renderable) {
+ Preconditions.checkNotNull(transformProvider, "Parameter \"transformProvider\" was null.");
+ Preconditions.checkNotNull(renderable, "Parameter \"renderable\" was null.");
+ this.transformProvider = transformProvider;
+ this.renderable = renderable;
+ entity = createFilamentEntity(EngineInstance.getEngine());
+
+ // SFB's can be imported with re-centering or scaling; rather than perform those operations to
+ // the vertices (and bones, &c) at import time, we keep vertex data in the same unit as the
+ // source asset and apply at runtime to a child entity via this relative transform. If we get
+ // back null, the relative transform is identity and the child entity path can be skipped.
+ @Nullable Matrix relativeTransform = getRelativeTransform();
+ if (relativeTransform != null) {
+ childEntity =
+ createFilamentChildEntity(EngineInstance.getEngine(), entity, relativeTransform);
+ }
+
+ createGltfModelInstance();
+
+ createFilamentAssetModelInstance();
+
+ ResourceManager.getInstance()
+ .getRenderableInstanceCleanupRegistry()
+ .register(this, new CleanupCallback(entity, childEntity));
+ }
+
+
+ void createFilamentAssetModelInstance() {
+ if (renderable.getRenderableData() instanceof RenderableInternalFilamentAssetData) {
+ RenderableInternalFilamentAssetData renderableData =
+ (RenderableInternalFilamentAssetData) renderable.getRenderableData();
+
+ Engine engine = EngineInstance.getEngine().getFilamentEngine();
+
+ AssetLoader loader =
+ new AssetLoader(
+ engine,
+ RenderableInternalFilamentAssetData.getMaterialProvider(),
+ EntityManager.get());
+
+ FilamentAsset createdAsset = renderableData.isGltfBinary ? loader.createAssetFromBinary(renderableData.gltfByteBuffer)
+ : loader.createAssetFromJson(renderableData.gltfByteBuffer);
+
+ if (createdAsset == null) {
+ throw new IllegalStateException("Failed to load gltf");
+ }
+
+ if (renderable.collisionShape == null) {
+ com.google.android.filament.Box box = createdAsset.getBoundingBox();
+ float[] halfExtent = box.getHalfExtent();
+ float[] center = box.getCenter();
+ renderable.collisionShape =
+ new Box(
+ new Vector3(halfExtent[0], halfExtent[1], halfExtent[2]).scaled(2.0f),
+ new Vector3(center[0], center[1], center[2]));
+ }
+
+ Function urlResolver = renderableData.urlResolver;
+ for (String uri : createdAsset.getResourceUris()) {
+ if (urlResolver == null) {
+ Log.e(TAG, "Failed to download uri " + uri + " no url resolver.");
+ continue;
+ }
+ Uri dataUri = urlResolver.apply(uri);
+ try {
+ Callable callable = LoadHelper.fromUri(renderableData.context, dataUri);
+ renderableData.resourceLoader.addResourceData(
+ uri, ByteBuffer.wrap(SceneformBufferUtils.inputStreamCallableToByteArray(callable)));
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to download data uri " + dataUri, e);
+ }
+ }
+ renderableData.resourceLoader.loadResources(createdAsset);
+
+ TransformManager transformManager = EngineInstance.getEngine().getTransformManager();
+
+ @EntityInstance int rootInstance = transformManager.getInstance(createdAsset.getRoot());
+ @EntityInstance
+ int parentInstance = transformManager.getInstance(childEntity == 0 ? entity : childEntity);
+
+ transformManager.setParent(rootInstance, parentInstance);
+
+ filamentAsset = createdAsset;
+ }
+ }
+
+
+ void createGltfModelInstance() {return ;}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @Nullable
+
+ public FilamentAsset getFilamentAsset() {
+ return filamentAsset;
+ }
+
+ /**
+ * Get the {@link Renderable} to display for this {@link RenderableInstance}.
+ *
+ * @return {@link Renderable} asset, usually a 3D model.
+ */
+ public Renderable getRenderable() {
+ return renderable;
+ }
+
+ public @Entity int getEntity() {
+ return entity;
+ }
+
+ public @Entity int getRenderedEntity() {
+ return (childEntity == 0) ? entity : childEntity;
+ }
+
+ void setModelMatrix(TransformManager transformManager, @Size(min = 16) float[] transform) {
+ // Use entity, rather than childEntity; setting the latter would slam the local transform which
+ // corrects for scaling and offset.
+ @EntityInstance int instance = transformManager.getInstance(entity);
+ transformManager.setTransform(instance, transform);
+ }
+
+ /** @hide */
+ public Matrix getWorldModelMatrix() {
+ return renderable.getFinalModelMatrix(transformProvider.getWorldModelMatrix());
+ }
+
+ public void setSkinningModifier(@Nullable SkinningModifier skinningModifier) {
+ this.skinningModifier = skinningModifier;
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private void setupSkeleton(IRenderableInternalData renderableInternalData) {return ;}
+
+
+
+ /** @hide */
+ public void prepareForDraw() {
+ renderable.prepareForDraw();
+
+ ChangeId changeId = renderable.getId();
+ if (changeId.checkChanged(renderableId)) {
+ IRenderableInternalData renderableInternalData = renderable.getRenderableData();
+ setupSkeleton(renderableInternalData);
+ renderableInternalData.buildInstanceData(renderable, getRenderedEntity());
+ renderableId = changeId.get();
+ // First time we're rendering, so always update the skinning even if we aren't animating and
+ // there is no skinModifier.
+ updateSkinning(true);
+ } else {
+ // Will only update the skinning if the renderable is animating or there is a skinModifier
+ // that has been changed since the last draw.
+ updateSkinning(false);
+ }
+ }
+
+
+ private void attachFilamentAssetToRenderer() {
+ FilamentAsset currentFilamentAsset = filamentAsset;
+ if (currentFilamentAsset != null) {
+ int[] entities = currentFilamentAsset.getEntities();
+ Preconditions.checkNotNull(attachedRenderer)
+ .getFilamentScene()
+ .addEntity(currentFilamentAsset.getRoot());
+ Preconditions.checkNotNull(attachedRenderer).getFilamentScene().addEntities(entities);
+ }
+ }
+
+ /** @hide */
+ public void attachToRenderer(Renderer renderer) {
+ renderer.addInstance(this);
+ attachedRenderer = renderer;
+ renderable.attachToRenderer(renderer);
+ attachFilamentAssetToRenderer();
+ }
+
+
+ void detachFilamentAssetFromRenderer() {
+ FilamentAsset currentFilamentAsset = filamentAsset;
+ if (currentFilamentAsset != null) {
+ int[] entities = currentFilamentAsset.getEntities();
+ for (int entity : entities) {
+ Preconditions.checkNotNull(attachedRenderer).getFilamentScene().removeEntity(entity);
+ }
+ int root = currentFilamentAsset.getRoot();
+ Preconditions.checkNotNull(attachedRenderer).getFilamentScene().removeEntity(root);
+ }
+ }
+
+ /** @hide */
+ public void detachFromRenderer() {
+ Renderer rendererToDetach = attachedRenderer;
+ if (rendererToDetach != null) {
+ detachFilamentAssetFromRenderer();
+ rendererToDetach.removeInstance(this);
+ renderable.detatchFromRenderer();
+ }
+ }
+
+ /**
+ * Returns the transform of this renderable relative to it's node. This will be non-null if the
+ * .sfa file includes a scale other than 1 or has recentering turned on.
+ *
+ * @hide
+ */
+ @Nullable
+ public Matrix getRelativeTransform() {
+ if (cachedRelativeTransform != null) {
+ return cachedRelativeTransform;
+ }
+
+ IRenderableInternalData renderableData = renderable.getRenderableData();
+ float scale = renderableData.getTransformScale();
+ Vector3 offset = renderableData.getTransformOffset();
+ if (scale == 1f && Vector3.equals(offset, Vector3.zero())) {
+ return null;
+ }
+
+ cachedRelativeTransform = new Matrix();
+ cachedRelativeTransform.makeScale(scale);
+ cachedRelativeTransform.setTranslation(offset);
+ return cachedRelativeTransform;
+ }
+
+ /**
+ * Returns the inverse transform of this renderable relative to it's node. This will be non-null
+ * if the .sfa file includes a scale other than 1 or has recentering turned on.
+ *
+ * @hide
+ */
+ @Nullable
+ public Matrix getRelativeTransformInverse() {
+ if (cachedRelativeTransformInverse != null) {
+ return cachedRelativeTransformInverse;
+ }
+
+ Matrix relativeTransform = getRelativeTransform();
+ if (relativeTransform == null) {
+ return null;
+ }
+
+ cachedRelativeTransformInverse = new Matrix();
+ Matrix.invert(relativeTransform, cachedRelativeTransformInverse);
+ return cachedRelativeTransformInverse;
+ }
+
+
+ private void updateSkinning(boolean force) {return ;}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ void setBlendOrderAt(int index, int blendOrder) {
+ RenderableManager renderableManager = EngineInstance.getEngine().getRenderableManager();
+ @EntityInstance int renderableInstance = renderableManager.getInstance(getRenderedEntity());
+ renderableManager.setBlendOrderAt(renderableInstance, index, blendOrder);
+ }
+
+ @Entity
+ private static int createFilamentEntity(IEngine engine) {
+ EntityManager entityManager = EntityManager.get();
+ @Entity int entity = entityManager.create();
+ TransformManager transformManager = engine.getTransformManager();
+ transformManager.create(entity);
+ return entity;
+ }
+
+ @Entity
+ private static int createFilamentChildEntity(
+ IEngine engine, @Entity int entity, Matrix relativeTransform) {
+ EntityManager entityManager = EntityManager.get();
+ @Entity int childEntity = entityManager.create();
+ TransformManager transformManager = engine.getTransformManager();
+ transformManager.create(
+ childEntity, transformManager.getInstance(entity), relativeTransform.data);
+ return childEntity;
+ }
+
+ /** Releases resources held by a {@link RenderableInstance} */
+ private static final class CleanupCallback implements Runnable {
+ private final int childEntity;
+ private final int entity;
+
+ CleanupCallback(int childEntity, int entity) {
+ this.childEntity = childEntity;
+ this.entity = entity;
+ }
+
+ @Override
+ public void run() {
+ AndroidPreconditions.checkUiThread();
+
+ IEngine engine = EngineInstance.getEngine();
+
+ if (engine == null || !engine.isValid()) {
+ return;
+ }
+
+ RenderableManager renderableManager = engine.getRenderableManager();
+
+ if (childEntity != 0) {
+ renderableManager.destroy(childEntity);
+ }
+ if (entity != 0) {
+ renderableManager.destroy(entity);
+ }
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableInternalData.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableInternalData.java
new file mode 100644
index 0000000..425f3c5
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableInternalData.java
@@ -0,0 +1,334 @@
+package com.google.ar.sceneform.rendering;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.filament.Box;
+import com.google.android.filament.Entity;
+import com.google.android.filament.EntityInstance;
+import com.google.android.filament.IndexBuffer;
+import com.google.android.filament.RenderableManager;
+import com.google.android.filament.VertexBuffer;
+
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+
+/**
+ * Represents the data used by a {@link Renderable} for rendering. All filament resources and
+ * materials contained here will be disposed when the {@link RenderableInternalData#dispose()}
+ * function is called.
+ */
+class RenderableInternalData implements IRenderableInternalData {
+ private static final String TAG = RenderableInternalData.class.getSimpleName();
+
+ /** Represents the data used to render each mesh of the renderable. */
+ static class MeshData {
+ // The start index into the triangle indices buffer for this mesh.
+ int indexStart;
+ // The end index into the triangle indices buffer for this mesh.
+ int indexEnd;
+ }
+
+ // Geometry data.
+ private final Vector3 centerAabb = Vector3.zero();
+ private final Vector3 extentsAabb = Vector3.zero();
+
+ // Transform data.
+ private float transformScale = 1f;
+ private final Vector3 transformOffset = Vector3.zero();
+
+ // Raw buffers.
+ @Nullable private IntBuffer rawIndexBuffer;
+ @Nullable private FloatBuffer rawPositionBuffer;
+ @Nullable private FloatBuffer rawTangentsBuffer;
+ @Nullable private FloatBuffer rawUvBuffer;
+ @Nullable private FloatBuffer rawColorBuffer;
+
+ // Filament Geometry buffers.
+ @Nullable private IndexBuffer indexBuffer;
+ @Nullable private VertexBuffer vertexBuffer;
+
+ // Represents the set of meshes to render.
+ private final ArrayList meshes = new ArrayList<>();
+
+
+
+
+
+ @Override
+ public void setCenterAabb(Vector3 minAabb) {
+ this.centerAabb.set(minAabb);
+ }
+
+ @Override
+ public Vector3 getCenterAabb() {
+ return new Vector3(centerAabb);
+ }
+
+ @Override
+ public void setExtentsAabb(Vector3 maxAabb) {
+ this.extentsAabb.set(maxAabb);
+ }
+
+ @Override
+ public Vector3 getExtentsAabb() {
+ return new Vector3(extentsAabb);
+ }
+
+ @Override
+ public Vector3 getSizeAabb() {
+ return extentsAabb.scaled(2.0f);
+ }
+
+ @Override
+ public void setTransformScale(float scale) {
+ this.transformScale = scale;
+ }
+
+ @Override
+ public float getTransformScale() {
+ return transformScale;
+ }
+
+ @Override
+ public void setTransformOffset(Vector3 offset) {
+ this.transformOffset.set(offset);
+ }
+
+ @Override
+ public Vector3 getTransformOffset() {
+ return new Vector3(transformOffset);
+ }
+
+ @Override
+ public ArrayList getMeshes() {
+ return meshes;
+ }
+
+ @Override
+ public void setIndexBuffer(@Nullable IndexBuffer indexBuffer) {
+ this.indexBuffer = indexBuffer;
+ }
+
+ @Override
+ @Nullable
+ public IndexBuffer getIndexBuffer() {
+ return indexBuffer;
+ }
+
+ @Override
+ public void setVertexBuffer(@Nullable VertexBuffer vertexBuffer) {
+ this.vertexBuffer = vertexBuffer;
+ }
+
+ @Override
+ @Nullable
+ public VertexBuffer getVertexBuffer() {
+ return vertexBuffer;
+ }
+
+ @Override
+ public void setRawIndexBuffer(@Nullable IntBuffer rawIndexBuffer) {
+ this.rawIndexBuffer = rawIndexBuffer;
+ }
+
+ @Override
+ @Nullable
+ public IntBuffer getRawIndexBuffer() {
+ return rawIndexBuffer;
+ }
+
+ @Override
+ public void setRawPositionBuffer(@Nullable FloatBuffer rawPositionBuffer) {
+ this.rawPositionBuffer = rawPositionBuffer;
+ }
+
+ @Override
+ @Nullable
+ public FloatBuffer getRawPositionBuffer() {
+ return rawPositionBuffer;
+ }
+
+ @Override
+ public void setRawTangentsBuffer(@Nullable FloatBuffer rawTangentsBuffer) {
+ this.rawTangentsBuffer = rawTangentsBuffer;
+ }
+
+ @Override
+ @Nullable
+ public FloatBuffer getRawTangentsBuffer() {
+ return rawTangentsBuffer;
+ }
+
+ @Override
+ public void setRawUvBuffer(@Nullable FloatBuffer rawUvBuffer) {
+ this.rawUvBuffer = rawUvBuffer;
+ }
+
+ @Override
+ @Nullable
+ public FloatBuffer getRawUvBuffer() {
+ return rawUvBuffer;
+ }
+
+ @Override
+ public void setRawColorBuffer(@Nullable FloatBuffer rawColorBuffer) {
+ this.rawColorBuffer = rawColorBuffer;
+ }
+
+ @Override
+ @Nullable
+ public FloatBuffer getRawColorBuffer() {
+ return rawColorBuffer;
+ }
+
+
+ private void setupSkeleton(RenderableManager.Builder builder) {return ;}
+
+
+
+
+
+ @Override
+ public void buildInstanceData(Renderable renderable, @Entity int renderedEntity) {
+ IRenderableInternalData renderableData = renderable.getRenderableData();
+ ArrayList materialBindings = renderable.getMaterialBindings();
+ RenderableManager renderableManager = EngineInstance.getEngine().getRenderableManager();
+ @EntityInstance int renderableInstance = renderableManager.getInstance(renderedEntity);
+
+ // Determine if a new filament Renderable needs to be created.
+ int meshCount = renderableData.getMeshes().size();
+ if (renderableInstance == 0
+ || renderableManager.getPrimitiveCount(renderableInstance) != meshCount) {
+ // Destroy the old one if it exists.
+ if (renderableInstance != 0) {
+ renderableManager.destroy(renderedEntity);
+ }
+
+ // Build the filament renderable.
+ RenderableManager.Builder builder =
+ new RenderableManager.Builder(meshCount)
+ .priority(renderable.getRenderPriority())
+ .castShadows(renderable.isShadowCaster())
+ .receiveShadows(renderable.isShadowReceiver());
+
+ setupSkeleton(builder);
+
+ builder.build(EngineInstance.getEngine().getFilamentEngine(), renderedEntity);
+
+ renderableInstance = renderableManager.getInstance(renderedEntity);
+ if (renderableInstance == 0) {
+ throw new AssertionError("Unable to create RenderableInstance.");
+ }
+ } else {
+ renderableManager.setPriority(renderableInstance, renderable.getRenderPriority());
+ renderableManager.setCastShadows(renderableInstance, renderable.isShadowCaster());
+ renderableManager.setReceiveShadows(renderableInstance, renderable.isShadowReceiver());
+ }
+
+ // Update the bounding box.
+ Vector3 extents = renderableData.getExtentsAabb();
+ Vector3 center = renderableData.getCenterAabb();
+ Box filamentBox = new Box(center.x, center.y, center.z, extents.x, extents.y, extents.z);
+ renderableManager.setAxisAlignedBoundingBox(renderableInstance, filamentBox);
+
+ if (materialBindings.size() != meshCount) {
+ throw new AssertionError("Material Bindings are out of sync with meshes.");
+ }
+
+ // Update the geometry and material instances.
+ final RenderableManager.PrimitiveType primitiveType = RenderableManager.PrimitiveType.TRIANGLES;
+ for (int mesh = 0; mesh < meshCount; ++mesh) {
+ // Update the geometry assigned to the filament renderable.
+ RenderableInternalData.MeshData meshData = renderableData.getMeshes().get(mesh);
+ @Nullable VertexBuffer vertexBuffer = renderableData.getVertexBuffer();
+ @Nullable IndexBuffer indexBuffer = renderableData.getIndexBuffer();
+ if (vertexBuffer == null || indexBuffer == null) {
+ throw new AssertionError("Internal Error: Failed to get vertex or index buffer");
+ }
+ renderableManager.setGeometryAt(
+ renderableInstance,
+ mesh,
+ primitiveType,
+ vertexBuffer,
+ indexBuffer,
+ meshData.indexStart,
+ meshData.indexEnd - meshData.indexStart);
+
+ // Update the material instances assigned to the filament renderable.
+ Material material = materialBindings.get(mesh);
+ renderableManager.setMaterialInstanceAt(
+ renderableInstance, mesh, material.getFilamentMaterialInstance());
+ }
+ }
+
+ @Override
+ public void setAnimationNames(@NonNull List animationNames) {}
+
+ @NonNull
+ @Override
+ public List getAnimationNames() {
+ return Collections.emptyList();
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /** @hide */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ ThreadPools.getMainExecutor().execute(() -> dispose());
+ } catch (Exception e) {
+ Log.e(TAG, "Error while Finalizing Renderable Internal Data.", e);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Removes any memory used by the object.
+ *
+ * @hide
+ */
+ @Override
+ public void dispose() {
+ AndroidPreconditions.checkUiThread();
+
+ IEngine engine = EngineInstance.getEngine();
+ if (engine == null || !engine.isValid()) {
+ return;
+ }
+
+ if (vertexBuffer != null) {
+ engine.destroyVertexBuffer(vertexBuffer);
+ vertexBuffer = null;
+ }
+
+ if (indexBuffer != null) {
+ engine.destroyIndexBuffer(indexBuffer);
+ indexBuffer = null;
+ }
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableInternalFilamentAssetData.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableInternalFilamentAssetData.java
new file mode 100644
index 0000000..336df20
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderableInternalFilamentAssetData.java
@@ -0,0 +1,223 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import android.net.Uri;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.filament.IndexBuffer;
+import com.google.android.filament.VertexBuffer;
+import com.google.android.filament.gltfio.MaterialProvider;
+import com.google.android.filament.gltfio.ResourceLoader;
+
+
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.RenderableInternalData.MeshData;
+import java.nio.Buffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+
+import java.util.List;
+
+import java.util.function.Function;
+
+/** Represents the data used by a {@link Renderable} for rendering natively loaded glTF data. */
+@SuppressWarnings("AndroidJdkLibsChecker")
+public class RenderableInternalFilamentAssetData implements IRenderableInternalData {
+
+ Context context;
+ Buffer gltfByteBuffer;
+ boolean isGltfBinary;
+ ResourceLoader resourceLoader;
+ @Nullable Function urlResolver;
+ static MaterialProvider materialProvider;
+
+ static MaterialProvider getMaterialProvider() {
+ if (materialProvider == null) {
+ materialProvider = new MaterialProvider(EngineInstance.getEngine().getFilamentEngine());
+ }
+ return materialProvider;
+ }
+
+ @Override
+ public void setCenterAabb(Vector3 center) {
+ // Not Implemented
+ }
+
+ @Override
+ public Vector3 getCenterAabb() {
+ // Not Implemented
+ return Vector3.zero();
+ }
+
+ @Override
+ public void setExtentsAabb(Vector3 halfExtents) {
+ // Not Implemented
+ }
+
+ @Override
+ public Vector3 getExtentsAabb() {
+ throw new IllegalStateException("Not Implemented");
+ }
+
+ @Override
+ public Vector3 getSizeAabb() {
+ // Not Implemented
+ return Vector3.zero();
+ }
+
+ @Override
+ public void setTransformScale(float scale) {
+ // Not Implemented
+ }
+
+ @Override
+ public float getTransformScale() {
+ // Not Implemented
+ return 1.0f;
+ }
+
+ @Override
+ public void setTransformOffset(Vector3 offset) {
+ // Not Implemented
+ }
+
+ @Override
+ public Vector3 getTransformOffset() {
+ // Not Implemented
+ return Vector3.zero();
+ }
+
+ @Override
+ public ArrayList getMeshes() {
+ // Not Implemented
+ return new ArrayList<>();
+ }
+
+ public ArrayList getMaterialBindingIds() {
+ // Not Implemented
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setIndexBuffer(@Nullable IndexBuffer indexBuffer) {
+ // Not Implemented
+ }
+
+ @Nullable
+ @Override
+ public IndexBuffer getIndexBuffer() {
+ // Not Implemented
+ return null;
+ }
+
+ @Override
+ public void setVertexBuffer(@Nullable VertexBuffer vertexBuffer) {
+ // Not Implemented
+ }
+
+ @Nullable
+ @Override
+ public VertexBuffer getVertexBuffer() {
+ // Not Implemented
+ return null;
+ }
+
+ @Override
+ public void setRawIndexBuffer(@Nullable IntBuffer rawIndexBuffer) {
+ // Not Implemented
+ }
+
+ @Nullable
+ @Override
+ public IntBuffer getRawIndexBuffer() {
+ // Not Implemented
+ return null;
+ }
+
+ @Override
+ public void setRawPositionBuffer(@Nullable FloatBuffer rawPositionBuffer) {
+ // Not Implemented
+ }
+
+ @Nullable
+ @Override
+ public FloatBuffer getRawPositionBuffer() {
+ // Not Implemented
+ return null;
+ }
+
+ @Override
+ public void setRawTangentsBuffer(@Nullable FloatBuffer rawTangentsBuffer) {
+ // Not Implemented
+ }
+
+ @Nullable
+ @Override
+ public FloatBuffer getRawTangentsBuffer() {
+ // Not Implemented
+ return null;
+ }
+
+ @Override
+ public void setRawUvBuffer(@Nullable FloatBuffer rawUvBuffer) {
+ // Not Implemented
+ }
+
+ @Nullable
+ @Override
+ public FloatBuffer getRawUvBuffer() {
+ // Not Implemented
+ return null;
+ }
+
+ @Override
+ public void setRawColorBuffer(@Nullable FloatBuffer rawColorBuffer) {
+ // Not Implemented
+ }
+
+ @Nullable
+ @Override
+ public FloatBuffer getRawColorBuffer() {
+ // Not Implemented
+ return null;
+ }
+
+ @Override
+ public void setAnimationNames(@NonNull List animationNames) {
+ // Not Implemented
+ }
+
+ @Override
+ @NonNull
+ public List getAnimationNames() {
+ // Not Implemented
+ return new ArrayList<>();
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @Override
+ public void buildInstanceData(Renderable renderable, int renderedEntity) {}
+
+ @Override
+ public void dispose() {}
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Renderer.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Renderer.java
new file mode 100644
index 0000000..b43b6b4
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Renderer.java
@@ -0,0 +1,625 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import android.os.Build;
+import android.view.Surface;
+import android.view.SurfaceView;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.filament.Camera;
+import com.google.android.filament.Entity;
+import com.google.android.filament.IndirectLight;
+import com.google.android.filament.Scene;
+import com.google.android.filament.SwapChain;
+import com.google.android.filament.TransformManager;
+import com.google.android.filament.View.DynamicResolutionOptions;
+import com.google.android.filament.Viewport;
+import com.google.android.filament.android.UiHelper;
+
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.EnvironmentalHdrParameters;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A rendering context.
+ *
+ * Contains everything that will be drawn on a surface.
+ *
+ * @hide Not a public facing API for version 1.0
+ */
+public class Renderer implements UiHelper.RendererCallback {
+ // Default camera settings are used everwhere that ARCore HDR Lighting (Deeplight) is disabled or
+ // unavailable.
+ private static final float DEFAULT_CAMERA_APERATURE = 4.0f;
+ private static final float DEFAULT_CAMERA_SHUTTER_SPEED = 1.0f / 30.0f;
+ private static final float DEFAULT_CAMERA_ISO = 320.0f;
+
+ // HDR lighting camera settings are chosen to provide an exposure value of 1.0. These are used
+ // when ARCore HDR Lighting is enabled in Sceneform.
+ private static final float ARCORE_HDR_LIGHTING_CAMERA_APERATURE = 1.0f;
+ private static final float ARCORE_HDR_LIGHTING_CAMERA_SHUTTER_SPEED = 1.2f;
+ private static final float ARCORE_HDR_LIGHTING_CAMERA_ISO = 100.0f;
+
+ private static final Color DEFAULT_CLEAR_COLOR = new Color(0.0f, 0.0f, 0.0f, 1.0f);
+
+ // Limit resolution to 1080p for the minor edge. This is enough for Filament.
+ private static final int MAXIMUM_RESOLUTION = 1080;
+
+ @Nullable private CameraProvider cameraProvider;
+ private final SurfaceView surfaceView;
+ private final ViewAttachmentManager viewAttachmentManager;
+
+ private final ArrayList renderableInstances = new ArrayList<>();
+ private final ArrayList lightInstances = new ArrayList<>();
+
+ private Surface surface;
+ @Nullable private SwapChain swapChain;
+ private com.google.android.filament.View view;
+ private com.google.android.filament.View emptyView;
+ private com.google.android.filament.Renderer renderer;
+ private Camera camera;
+ private Scene scene;
+ private IndirectLight indirectLight;
+ private boolean recreateSwapChain;
+
+ private float cameraAperature;
+ private float cameraShutterSpeed;
+ private float cameraIso;
+
+ private UiHelper filamentHelper;
+
+ private final double[] cameraProjectionMatrix = new double[16];
+
+ private EnvironmentalHdrParameters environmentalHdrParameters =
+ EnvironmentalHdrParameters.makeDefault();
+
+ private static class Mirror {
+ @Nullable SwapChain swapChain;
+ @Nullable Surface surface;
+ Viewport viewport;
+ }
+
+ private final List mirrors = new ArrayList<>();
+
+ /** @hide */
+ public interface PreRenderCallback {
+ void preRender(
+ com.google.android.filament.Renderer renderer,
+ com.google.android.filament.SwapChain swapChain,
+ com.google.android.filament.Camera camera);
+ }
+
+ @Nullable private Runnable onFrameRenderDebugCallback = null;
+ @Nullable private PreRenderCallback preRenderCallback;
+
+ /** @hide */
+ @SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public Renderer(SurfaceView view) {
+ Preconditions.checkNotNull(view, "Parameter \"view\" was null.");
+ // Enforce api level 24
+ AndroidPreconditions.checkMinAndroidApiLevel();
+
+ this.surfaceView = view;
+ viewAttachmentManager = new ViewAttachmentManager(getContext(), view);
+ initialize();
+ }
+
+ /**
+ * Starts mirroring to the specified {@link Surface}.
+ *
+ * @hide
+ */
+ public void startMirroring(Surface surface, int left, int bottom, int width, int height) {
+ Mirror mirror = new Mirror();
+ mirror.surface = surface;
+ mirror.viewport = new Viewport(left, bottom, width, height);
+ mirror.swapChain = null;
+ synchronized (mirrors) {
+ mirrors.add(mirror);
+ }
+ }
+
+ /**
+ * Stops mirroring to the specified {@link Surface}.
+ *
+ * @hide
+ */
+ public void stopMirroring(Surface surface) {
+ synchronized (mirrors) {
+ for (Mirror mirror : mirrors) {
+ if (mirror.surface == surface) {
+ mirror.surface = null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Access to the underlying Filament renderer.
+ *
+ * @hide
+ */
+ public com.google.android.filament.Renderer getFilamentRenderer() {
+ return renderer;
+ }
+
+ public SurfaceView getSurfaceView() {
+ return surfaceView;
+ }
+
+ /** @hide */
+ public void setClearColor(Color color) {
+ com.google.android.filament.Renderer.ClearOptions options = new com.google.android.filament.Renderer.ClearOptions();
+ options.clearColor[0] = color.r;
+ options.clearColor[1] = color.g;
+ options.clearColor[2] = color.b;
+ options.clearColor[3] = color.a;
+ renderer.setClearOptions(options);
+ }
+
+ /** @hide */
+ public void setDefaultClearColor() {
+ setClearColor(DEFAULT_CLEAR_COLOR);
+ }
+
+ /**
+ * Inverts winding for front face rendering.
+ *
+ * @hide Used internally by ArSceneView
+ */
+
+ public void setFrontFaceWindingInverted(Boolean inverted) {
+ view.setFrontFaceWindingInverted(inverted);
+ }
+
+ /**
+ * Checks whether winding is inverted for front face rendering.
+ *
+ * @hide Used internally by ViewRenderable
+ */
+
+ public boolean isFrontFaceWindingInverted() {
+ return view.isFrontFaceWindingInverted();
+ }
+
+ /** @hide */
+ public void setCameraProvider(@Nullable CameraProvider cameraProvider) {
+ this.cameraProvider = cameraProvider;
+ }
+
+ /** @hide */
+ public void onPause() {
+ viewAttachmentManager.onPause();
+ }
+
+ /** @hide */
+ public void onResume() {
+ viewAttachmentManager.onResume();
+ }
+
+ /**
+ * Sets a callback to happen after each frame is rendered. This can be used to log performance
+ * metrics for a given frame.
+ */
+ public void setFrameRenderDebugCallback(Runnable onFrameRenderDebugCallback) {
+ this.onFrameRenderDebugCallback = onFrameRenderDebugCallback;
+ }
+
+ private Viewport getLetterboxViewport(Viewport srcViewport, Viewport destViewport) {
+ boolean letterBoxSides =
+ (destViewport.width / (float) destViewport.height)
+ > (srcViewport.width / (float) srcViewport.height);
+ float scale =
+ letterBoxSides
+ ? (destViewport.height / (float) srcViewport.height)
+ : (destViewport.width / (float) srcViewport.width);
+ int width = (int) (srcViewport.width * scale);
+ int height = (int) (srcViewport.height * scale);
+ int left = (destViewport.width - width) / 2;
+ int bottom = (destViewport.height - height) / 2;
+ return new Viewport(left, bottom, width, height);
+ }
+
+ /** @hide */
+ public void setPreRenderCallback(@Nullable PreRenderCallback preRenderCallback) {
+ this.preRenderCallback = preRenderCallback;
+ }
+
+ /** @hide */
+ public void render(boolean debugEnabled, long frameTimeNanos) {
+ synchronized (this) {
+ if (recreateSwapChain) {
+ final IEngine engine = EngineInstance.getEngine();
+ if (swapChain != null) {
+ engine.destroySwapChain(swapChain);
+ }
+ swapChain = engine.createSwapChain(surface, SwapChain.CONFIG_READABLE);
+ recreateSwapChain = false;
+ }
+ }
+ synchronized (mirrors) {
+ Iterator mirrorIterator = mirrors.iterator();
+ while (mirrorIterator.hasNext()) {
+ Mirror mirror = mirrorIterator.next();
+ if (mirror.surface == null) {
+ if (mirror.swapChain != null) {
+ final IEngine engine = EngineInstance.getEngine();
+ engine.destroySwapChain(Preconditions.checkNotNull(mirror.swapChain));
+ }
+ mirrorIterator.remove();
+ } else if (mirror.swapChain == null) {
+ final IEngine engine = EngineInstance.getEngine();
+ mirror.swapChain = engine.createSwapChain(Preconditions.checkNotNull(mirror.surface));
+ }
+ }
+ }
+
+ if (filamentHelper.isReadyToRender() || EngineInstance.isHeadlessMode()) {
+ updateInstances();
+ updateLights();
+
+ CameraProvider cameraProvider = this.cameraProvider;
+ if (cameraProvider != null) {
+ final float[] projectionMatrixData = cameraProvider.getProjectionMatrix().data;
+ for (int i = 0; i < 16; ++i) {
+ cameraProjectionMatrix[i] = projectionMatrixData[i];
+ }
+
+ camera.setModelMatrix(cameraProvider.getWorldModelMatrix().data);
+ camera.setCustomProjection(
+ cameraProjectionMatrix,
+ cameraProvider.getNearClipPlane(),
+ cameraProvider.getFarClipPlane());
+ @Nullable SwapChain swapChainLocal = swapChain;
+ if (swapChainLocal == null) {
+ throw new AssertionError("Internal Error: Failed to get swap chain");
+ }
+
+ if (renderer.beginFrame(swapChainLocal, frameTimeNanos)) {
+ if (preRenderCallback != null) {
+ preRenderCallback.preRender(renderer, swapChainLocal, camera);
+ }
+
+ // Currently, filament does not provide functionality for disabling cameras, and
+ // rendering a view with a null camera doesn't clear the viewport. As a workaround, we
+ // render an empty view when the camera is disabled. this is actually similar to what we
+ // need to do in the future if we want to add multiple camera support anyways. filament
+ // only allows one camera per-view, so for multiple cameras you need to create multiple
+ // views pointing to the same scene.
+ com.google.android.filament.View currentView =
+ cameraProvider.isActive() ? view : emptyView;
+ renderer.render(currentView);
+
+ synchronized (mirrors) {
+ for (Mirror mirror : mirrors) {
+ if (mirror.swapChain != null) {
+ renderer.mirrorFrame(
+ mirror.swapChain,
+ getLetterboxViewport(currentView.getViewport(), mirror.viewport),
+ currentView.getViewport(),
+ com.google.android.filament.Renderer.MIRROR_FRAME_FLAG_COMMIT
+ | com.google.android.filament.Renderer
+ .MIRROR_FRAME_FLAG_SET_PRESENTATION_TIME
+ | com.google.android.filament.Renderer.MIRROR_FRAME_FLAG_CLEAR);
+ }
+ }
+ }
+ if (onFrameRenderDebugCallback != null) {
+ onFrameRenderDebugCallback.run();
+ }
+ renderer.endFrame();
+ }
+
+ reclaimReleasedResources();
+ }
+ }
+ }
+
+ /** @hide */
+ public void dispose() {
+ filamentHelper.detach(); // call this before destroying the Engine (it could call back)
+
+ final IEngine engine = EngineInstance.getEngine();
+ if (indirectLight != null) {
+ engine.destroyIndirectLight(indirectLight);
+ }
+ engine.destroyRenderer(renderer);
+ engine.destroyView(view);
+ reclaimReleasedResources();
+ }
+
+ public Context getContext() {
+ return getSurfaceView().getContext();
+ }
+
+ /**
+ * Set the Light Probe used for reflections and indirect light.
+ *
+ * @hide the scene level API is publicly exposed, this is used by the Scene internally.
+ */
+ public void setLightProbe(LightProbe lightProbe) {
+ if (lightProbe == null) {
+ throw new AssertionError("Passed in an invalid light probe.");
+ }
+ final IndirectLight latestIndirectLight = lightProbe.buildIndirectLight();
+ if (latestIndirectLight != null) {
+ scene.setIndirectLight(latestIndirectLight);
+ if (indirectLight != null && indirectLight != latestIndirectLight) {
+ final IEngine engine = EngineInstance.getEngine();
+ engine.destroyIndirectLight(indirectLight);
+ }
+ indirectLight = latestIndirectLight;
+ }
+ }
+
+ /** @hide */
+ public void setDesiredSize(int width, int height) {
+ int minor = Math.min(width, height);
+ int major = Math.max(width, height);
+ if (minor > MAXIMUM_RESOLUTION) {
+ major = (major * MAXIMUM_RESOLUTION) / minor;
+ minor = MAXIMUM_RESOLUTION;
+ }
+ if (width < height) {
+ int t = minor;
+ minor = major;
+ major = t;
+ }
+
+ filamentHelper.setDesiredSize(major, minor);
+ }
+
+ /** @hide */
+ public int getDesiredWidth() {
+ return filamentHelper.getDesiredWidth();
+ }
+
+ /** @hide */
+ public int getDesiredHeight() {
+ return filamentHelper.getDesiredHeight();
+ }
+
+ /** @hide UiHelper.RendererCallback implementation */
+ @Override
+ public void onNativeWindowChanged(Surface surface) {
+ synchronized (this) {
+ this.surface = surface;
+ recreateSwapChain = true;
+ }
+ }
+
+ /** @hide UiHelper.RendererCallback implementation */
+ @Override
+ public void onDetachedFromSurface() {
+ @Nullable SwapChain swapChainLocal = swapChain;
+ if (swapChainLocal != null) {
+ final IEngine engine = EngineInstance.getEngine();
+ engine.destroySwapChain(swapChainLocal);
+ // Required to ensure we don't return before Filament is done executing the
+ // destroySwapChain command, otherwise Android might destroy the Surface
+ // too early
+ engine.flushAndWait();
+ swapChain = null;
+ }
+ }
+
+ /** @hide Only used for scuba testing for now. */
+ public void setDynamicResolutionEnabled(boolean isEnabled) {
+ // Enable dynamic resolution. By default it will scale down to 25% of the screen area
+ // (i.e.: 50% on each axis, e.g.: reducing a 1080p image down to 720p).
+ // This can be changed in the options below.
+ // TODO: This functionality should probably be exposed to the developer eventually.
+ DynamicResolutionOptions options = new DynamicResolutionOptions();
+ options.enabled = isEnabled;
+ view.setDynamicResolutionOptions(options);
+ }
+
+ /** @hide Only used for scuba testing for now. */
+ @VisibleForTesting
+ public void setAntiAliasing(com.google.android.filament.View.AntiAliasing antiAliasing) {
+ view.setAntiAliasing(antiAliasing);
+ }
+
+ /** @hide Only used for scuba testing for now. */
+ @VisibleForTesting
+ public void setDithering(com.google.android.filament.View.Dithering dithering) {
+ view.setDithering(dithering);
+ }
+
+ /** @hide Used internally by ArSceneView. */
+
+ public void setPostProcessingEnabled(boolean enablePostProcessing) {return ;}
+
+
+
+ /** @hide Used internally by ArSceneView */
+
+ public void setRenderQuality(com.google.android.filament.View.RenderQuality renderQuality) {return ;}
+
+
+
+ /**
+ * Sets a high performance configuration for the filament view. Disables MSAA, disables
+ * post-process, disables dynamic resolution, sets quality to 'low'.
+ *
+ * @hide Used internally by ArSceneView
+ */
+
+ public void enablePerformanceMode() {return ;}
+
+
+
+
+
+
+
+
+
+ /**
+ * Getter to help convert between filament and Environmental HDR.
+ *
+ * @hide This may be removed in the future
+ */
+ public EnvironmentalHdrParameters getEnvironmentalHdrParameters() {
+ return environmentalHdrParameters;
+ }
+
+ /**
+ * Setter to help convert between filament and Environmental HDR.
+ *
+ * @hide This may be removed in the future
+ */
+ public void setEnvironmentalHdrParameters(EnvironmentalHdrParameters environmentalHdrParameters) {
+ this.environmentalHdrParameters = environmentalHdrParameters;
+ }
+
+ /** @hide UiHelper.RendererCallback implementation */
+ @Override
+ public void onResized(int width, int height) {
+ view.setViewport(new Viewport(0, 0, width, height));
+ emptyView.setViewport(new Viewport(0, 0, width, height));
+ }
+
+ /** @hide */
+ void addLight(LightInstance instance) {
+ @Entity int entity = instance.getEntity();
+ scene.addEntity(entity);
+ lightInstances.add(instance);
+ }
+
+ /** @hide */
+ void removeLight(LightInstance instance) {
+ @Entity int entity = instance.getEntity();
+ scene.remove(entity);
+ lightInstances.remove(instance);
+ }
+
+
+ private void addModelInstanceInternal(RenderableInstance instance) {return ;}
+
+
+
+
+
+
+ private void removeModelInstanceInternal(RenderableInstance instance) {return ;}
+
+
+
+
+
+ /** @hide */
+ void addInstance(RenderableInstance instance) {
+ scene.addEntity(instance.getRenderedEntity());
+ addModelInstanceInternal(instance);
+ renderableInstances.add(instance);
+ }
+
+ /** @hide */
+ void removeInstance(RenderableInstance instance) {
+ removeModelInstanceInternal(instance);
+ scene.remove(instance.getRenderedEntity());
+ renderableInstances.remove(instance);
+ }
+
+ Scene getFilamentScene() {
+ return scene;
+ }
+
+ ViewAttachmentManager getViewAttachmentManager() {
+ return viewAttachmentManager;
+ }
+
+ @SuppressWarnings("AndroidApiChecker") // CompletableFuture
+ private void initialize() {
+ SurfaceView surfaceView = getSurfaceView();
+
+ filamentHelper = new UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK);
+ filamentHelper.setRenderCallback(this);
+ filamentHelper.attachTo(surfaceView);
+
+ IEngine engine = EngineInstance.getEngine();
+
+ renderer = engine.createRenderer();
+ scene = engine.createScene();
+ view = engine.createView();
+ emptyView = engine.createView();
+ camera = engine.createCamera();
+ setUseHdrLightEstimate(false);
+
+ setDefaultClearColor();
+ view.setCamera(camera);
+ view.setScene(scene);
+
+ setDynamicResolutionEnabled(true);
+
+ emptyView.setCamera(engine.createCamera());
+ emptyView.setScene(engine.createScene());
+ }
+
+ public void setUseHdrLightEstimate(boolean useHdrLightEstimate) {
+ if (useHdrLightEstimate) {
+ cameraAperature = ARCORE_HDR_LIGHTING_CAMERA_APERATURE;
+ cameraShutterSpeed = ARCORE_HDR_LIGHTING_CAMERA_SHUTTER_SPEED;
+ cameraIso = ARCORE_HDR_LIGHTING_CAMERA_ISO;
+ } else {
+ cameraAperature = DEFAULT_CAMERA_APERATURE;
+ cameraShutterSpeed = DEFAULT_CAMERA_SHUTTER_SPEED;
+ cameraIso = DEFAULT_CAMERA_ISO;
+ }
+ // Setup the Camera Exposure values.
+ camera.setExposure(cameraAperature, cameraShutterSpeed, cameraIso);
+ }
+
+ /**
+ * Returns the exposure setting for renderering.
+ *
+ * @hide This is support deeplight API which is not stable yet.
+ */
+
+ public float getExposure() {
+ float e = (cameraAperature * cameraAperature) / cameraShutterSpeed * 100.0f / cameraIso;
+ return 1.0f / (1.2f * e);
+ }
+
+ private void updateInstances() {
+ final IEngine engine = EngineInstance.getEngine();
+ final TransformManager transformManager = engine.getTransformManager();
+ transformManager.openLocalTransformTransaction();
+
+ for (RenderableInstance renderableInstance : renderableInstances) {
+ renderableInstance.prepareForDraw();
+
+ float[] transform = renderableInstance.getWorldModelMatrix().data;
+ renderableInstance.setModelMatrix(transformManager, transform);
+ }
+
+ transformManager.commitLocalTransformTransaction();
+ }
+
+ private void updateLights() {
+ for (LightInstance lightInstance : lightInstances) {
+ lightInstance.updateTransform();
+ }
+ }
+
+ /**
+ * Releases rendering resources ready for garbage collection
+ *
+ * @return Count of resources currently in use
+ */
+ public static long reclaimReleasedResources() {
+ return ResourceManager.getInstance().reclaimReleasedResources();
+ }
+
+ /** Immediately releases all rendering resources, even if in use. */
+ public static void destroyAllResources() {
+ ResourceManager.getInstance().destroyAllResources();
+ EngineInstance.destroyEngine();
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderingResources.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderingResources.java
new file mode 100644
index 0000000..780310d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/RenderingResources.java
@@ -0,0 +1,114 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+
+import com.google.ar.sceneform.utilities.LoadHelper;
+
+final class RenderingResources {
+
+ public static enum Resource {
+ CAMERA_MATERIAL,
+ OPAQUE_COLORED_MATERIAL,
+ TRANSPARENT_COLORED_MATERIAL,
+ OPAQUE_TEXTURED_MATERIAL,
+ TRANSPARENT_TEXTURED_MATERIAL,
+ PLANE_SHADOW_MATERIAL,
+ PLANE_MATERIAL,
+ PLANE,
+ VIEW_RENDERABLE_MATERIAL,
+ };
+
+
+ private static int GetSceneformSourceResource(Context context, Resource resource) {
+ switch (resource) {
+ case CAMERA_MATERIAL:
+ return LoadHelper.rawResourceNameToIdentifier(context, "sceneform_camera_material");
+ case OPAQUE_COLORED_MATERIAL:
+ return LoadHelper.rawResourceNameToIdentifier(context, "sceneform_opaque_colored_material");
+ case TRANSPARENT_COLORED_MATERIAL:
+ return LoadHelper.rawResourceNameToIdentifier(
+ context, "sceneform_transparent_colored_material");
+ case OPAQUE_TEXTURED_MATERIAL:
+ return LoadHelper.rawResourceNameToIdentifier(
+ context, "sceneform_opaque_textured_material");
+ case TRANSPARENT_TEXTURED_MATERIAL:
+ return LoadHelper.rawResourceNameToIdentifier(
+ context, "sceneform_transparent_textured_material");
+ case PLANE_SHADOW_MATERIAL:
+ return LoadHelper.rawResourceNameToIdentifier(context, "sceneform_plane_shadow_material");
+ case PLANE_MATERIAL:
+ return LoadHelper.rawResourceNameToIdentifier(context, "sceneform_plane_material");
+ case PLANE:
+ return LoadHelper.drawableResourceNameToIdentifier(context, "sceneform_plane");
+ case VIEW_RENDERABLE_MATERIAL:
+ return LoadHelper.rawResourceNameToIdentifier(context, "sceneform_view_material");
+ }
+ return 0;
+ }
+
+
+ private static int GetMaterialFactoryBlazeResource(Resource resource) {return 0;}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private static int GetViewRenderableBlazeResource(Resource resource) {return 0;}
+
+
+
+
+
+
+
+
+
+ private static int GetSceneformBlazeResource(Resource resource) {return 0;}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public static int GetSceneformResource(Context context, Resource resource) {
+ int blazeResource = GetSceneformBlazeResource(resource);
+ if (blazeResource != 0) {
+ return blazeResource;
+ }
+ return GetSceneformSourceResource(context, resource);
+ }
+
+ private RenderingResources() {}
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ResourceHelper.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ResourceHelper.java
new file mode 100644
index 0000000..9545184
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ResourceHelper.java
@@ -0,0 +1,42 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+
+/** Helper class for loading resources in filament. */
+class ResourceHelper {
+ static ByteBuffer readResource(Context context, int resourceId) {
+ ByteBuffer buffer = null;
+ if (context != null) {
+ int length = 0;
+ try {
+ InputStream inputStream = context.getResources().openRawResource(resourceId);
+ // to get the length for use in 'allocateDirect'
+ inputStream.mark(-1); // no read limit
+ while (inputStream.read() != -1) {
+ length++;
+ }
+ // reset stream to beginning
+ inputStream.reset();
+
+ buffer = ByteBuffer.allocateDirect(length);
+ final ReadableByteChannel source = Channels.newChannel(inputStream);
+ try {
+ source.read(buffer);
+ } finally {
+ source.close();
+ }
+ buffer.rewind();
+ } catch (IOException exception) {
+ exception.printStackTrace();
+ buffer = null;
+ }
+ }
+
+ return buffer;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ResourceManager.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ResourceManager.java
new file mode 100644
index 0000000..45f7b0d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ResourceManager.java
@@ -0,0 +1,116 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.Nullable;
+
+import com.google.ar.sceneform.resources.ResourceHolder;
+import com.google.ar.sceneform.resources.ResourceRegistry;
+import java.util.ArrayList;
+
+/**
+ * Minimal resource manager. Maintains mappings from ids to created resources and a task executor
+ * dedicated to loading resources asynchronously.
+ *
+ * @hide
+ */
+@SuppressWarnings("initialization") // Suppress @UnderInitialization warning.
+public class ResourceManager {
+ @Nullable private static ResourceManager instance = null;
+
+ private final ArrayList resourceHolders = new ArrayList<>();
+ private final ResourceRegistry textureRegistry = new ResourceRegistry<>();
+ private final ResourceRegistry materialRegistry = new ResourceRegistry<>();
+ private final ResourceRegistry modelRenderableRegistry =
+ new ResourceRegistry<>();
+
+
+ private final ResourceRegistry viewRenderableRegistry = new ResourceRegistry<>();
+
+ private final CleanupRegistry cameraStreamCleanupRegistry = new CleanupRegistry<>();
+ private final CleanupRegistry externalTextureCleanupRegistry =
+ new CleanupRegistry<>();
+ private final CleanupRegistry materialCleanupRegistry = new CleanupRegistry<>();
+ private final CleanupRegistry renderableInstanceCleanupRegistry =
+ new CleanupRegistry<>();
+ private final CleanupRegistry textureCleanupRegistry = new CleanupRegistry<>();
+
+ ResourceRegistry getTextureRegistry() {
+ return textureRegistry;
+ }
+
+ ResourceRegistry getMaterialRegistry() {
+ return materialRegistry;
+ }
+
+ ResourceRegistry getModelRenderableRegistry() {
+ return modelRenderableRegistry;
+ }
+
+
+ ResourceRegistry getViewRenderableRegistry() {
+ return viewRenderableRegistry;
+ }
+
+ CleanupRegistry getCameraStreamCleanupRegistry() {
+ return cameraStreamCleanupRegistry;
+ }
+
+ CleanupRegistry getExternalTextureCleanupRegistry() {
+ return externalTextureCleanupRegistry;
+ }
+
+ CleanupRegistry getMaterialCleanupRegistry() {
+ return materialCleanupRegistry;
+ }
+
+ CleanupRegistry getRenderableInstanceCleanupRegistry() {
+ return renderableInstanceCleanupRegistry;
+ }
+
+ CleanupRegistry getTextureCleanupRegistry() {
+ return textureCleanupRegistry;
+ }
+
+ public long reclaimReleasedResources() {
+ long resourcesInUse = 0;
+ for (ResourceHolder registry : resourceHolders) {
+ resourcesInUse += registry.reclaimReleasedResources();
+ }
+ return resourcesInUse;
+ }
+
+ /** Forcibly deletes all tracked references */
+ public void destroyAllResources() {
+ for (ResourceHolder resourceHolder : resourceHolders) {
+ resourceHolder.destroyAllResources();
+ }
+ }
+
+ public void addResourceHolder(ResourceHolder resource) {
+ resourceHolders.add(resource);
+ }
+
+ public static ResourceManager getInstance() {
+ if (instance == null) {
+ instance = new ResourceManager();
+ }
+
+ return instance;
+ }
+
+ private ResourceManager() {
+ addResourceHolder(textureRegistry);
+ addResourceHolder(materialRegistry);
+ addResourceHolder(modelRenderableRegistry);
+ addViewRenderableRegistry();
+ addResourceHolder(cameraStreamCleanupRegistry);
+ addResourceHolder(externalTextureCleanupRegistry);
+ addResourceHolder(materialCleanupRegistry);
+ addResourceHolder(renderableInstanceCleanupRegistry);
+ addResourceHolder(textureCleanupRegistry);
+ }
+
+
+ private void addViewRenderableRegistry() {
+ addResourceHolder(viewRenderableRegistry);
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/SceneformBundle.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/SceneformBundle.java
new file mode 100644
index 0000000..5d33a49
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/SceneformBundle.java
@@ -0,0 +1,87 @@
+package com.google.ar.sceneform.rendering;
+
+import androidx.annotation.Nullable;
+import com.google.ar.sceneform.collision.Box;
+import com.google.ar.sceneform.collision.CollisionShape;
+import com.google.ar.sceneform.collision.Sphere;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.schemas.sceneform.CollisionShapeType;
+import com.google.ar.schemas.sceneform.SceneformBundleDef;
+import com.google.ar.schemas.sceneform.SuggestedCollisionShapeDef;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Helper functions for loading and processing rendercore bundles.
+ *
+ * @hide
+ */
+public final class SceneformBundle {
+ private static final String TAG = SceneformBundle.class.getSimpleName();
+ // TODO: This 'version range' is too narrow
+ public static final float RCB_MAJOR_VERSION = 0.54f;
+ public static final int RCB_MINOR_VERSION = 2;
+ private static final char[] RCB_SIGNATURE = {'R', 'B', 'U', 'N'};
+ // Per flatbuffer documentation, a buffer signature is written to
+ // bytes 4 - 7 inclusively.
+ private static final int SIGNATURE_OFFSET = 4;
+
+ static class VersionException extends Exception {
+ public VersionException(String message) {
+ super(message);
+ }
+ }
+
+ @Nullable
+ public static SceneformBundleDef tryLoadSceneformBundle(ByteBuffer buffer)
+ throws VersionException {
+
+ // Test the file signature to see if this is a real Rendercore Bundle.
+ if (isSceneformBundle(buffer)) {
+ buffer.rewind();
+ SceneformBundleDef bundle = SceneformBundleDef.getRootAsSceneformBundleDef(buffer);
+ float majorVersion = bundle.version().majorVersion();
+ int minorVersion = bundle.version().minorVersion();
+ if (RCB_MAJOR_VERSION < bundle.version().majorVersion()) {
+ throw new VersionException(
+ "Sceneform bundle (.sfb) version not supported, max version supported is "
+ + RCB_MAJOR_VERSION
+ + ".X. Version requested for loading is "
+ + majorVersion
+ + "."
+ + minorVersion);
+ }
+ return bundle;
+ }
+
+ return null;
+ }
+
+ public static CollisionShape readCollisionGeometry(SceneformBundleDef rcb) throws IOException {
+ SuggestedCollisionShapeDef shape = rcb.suggestedCollisionShape();
+ int type = shape.type();
+ switch (type) {
+ case CollisionShapeType.Box:
+ Box box = new Box();
+ box.setCenter(new Vector3(shape.center().x(), shape.center().y(), shape.center().z()));
+ box.setSize(new Vector3(shape.size().x(), shape.size().y(), shape.size().z()));
+ return box;
+ case CollisionShapeType.Sphere:
+ Sphere sphere = new Sphere();
+ sphere.setCenter(new Vector3(shape.center().x(), shape.center().y(), shape.center().z()));
+ sphere.setRadius(shape.size().x());
+ return sphere;
+ default:
+ throw new IOException("Invalid collisionCollisionGeometry type.");
+ }
+ }
+
+ public static boolean isSceneformBundle(ByteBuffer buffer) {
+ for (int i = 0; i < RCB_SIGNATURE.length; ++i) {
+ if (buffer.get(SIGNATURE_OFFSET + i) != RCB_SIGNATURE[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ShapeFactory.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ShapeFactory.java
new file mode 100644
index 0000000..d852c0d
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ShapeFactory.java
@@ -0,0 +1,395 @@
+package com.google.ar.sceneform.rendering;
+
+import android.os.Build;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.ar.sceneform.math.Vector3;
+import com.google.ar.sceneform.rendering.RenderableDefinition.Submesh;
+import com.google.ar.sceneform.rendering.Vertex.UvCoordinate;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+/** Utility class used to dynamically construct {@link ModelRenderable}s for various shapes. */
+@RequiresApi(api = Build.VERSION_CODES.N)
+public final class ShapeFactory {
+ private static final String TAG = ShapeFactory.class.getSimpleName();
+ private static final int COORDS_PER_TRIANGLE = 3;
+
+ /**
+ * Creates a {@link ModelRenderable} in the shape of a cube with the give specifications.
+ *
+ * @param size the size of the constructed cube
+ * @param center the center of the constructed cube
+ * @param material the material to use for rendering the cube
+ * @return renderable representing a cube with the given parameters
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ // CompletableFuture requires api level 24
+ public static ModelRenderable makeCube(Vector3 size, Vector3 center, Material material) {
+ AndroidPreconditions.checkMinAndroidApiLevel();
+
+ Vector3 extents = size.scaled(0.5f);
+
+ Vector3 p0 = Vector3.add(center, new Vector3(-extents.x, -extents.y, extents.z));
+ Vector3 p1 = Vector3.add(center, new Vector3(extents.x, -extents.y, extents.z));
+ Vector3 p2 = Vector3.add(center, new Vector3(extents.x, -extents.y, -extents.z));
+ Vector3 p3 = Vector3.add(center, new Vector3(-extents.x, -extents.y, -extents.z));
+ Vector3 p4 = Vector3.add(center, new Vector3(-extents.x, extents.y, extents.z));
+ Vector3 p5 = Vector3.add(center, new Vector3(extents.x, extents.y, extents.z));
+ Vector3 p6 = Vector3.add(center, new Vector3(extents.x, extents.y, -extents.z));
+ Vector3 p7 = Vector3.add(center, new Vector3(-extents.x, extents.y, -extents.z));
+
+ Vector3 up = Vector3.up();
+ Vector3 down = Vector3.down();
+ Vector3 front = Vector3.forward();
+ Vector3 back = Vector3.back();
+ Vector3 left = Vector3.left();
+ Vector3 right = Vector3.right();
+
+ Vertex.UvCoordinate uv00 = new Vertex.UvCoordinate(0.0f, 0.0f);
+ Vertex.UvCoordinate uv10 = new Vertex.UvCoordinate(1.0f, 0.0f);
+ Vertex.UvCoordinate uv01 = new Vertex.UvCoordinate(0.0f, 1.0f);
+ Vertex.UvCoordinate uv11 = new Vertex.UvCoordinate(1.0f, 1.0f);
+
+ ArrayList vertices =
+ new ArrayList<>(
+ Arrays.asList(
+ // Bottom
+ Vertex.builder().setPosition(p0).setNormal(down).setUvCoordinate(uv01).build(),
+ Vertex.builder().setPosition(p1).setNormal(down).setUvCoordinate(uv11).build(),
+ Vertex.builder().setPosition(p2).setNormal(down).setUvCoordinate(uv10).build(),
+ Vertex.builder().setPosition(p3).setNormal(down).setUvCoordinate(uv00).build(),
+ // Left
+ Vertex.builder().setPosition(p7).setNormal(left).setUvCoordinate(uv01).build(),
+ Vertex.builder().setPosition(p4).setNormal(left).setUvCoordinate(uv11).build(),
+ Vertex.builder().setPosition(p0).setNormal(left).setUvCoordinate(uv10).build(),
+ Vertex.builder().setPosition(p3).setNormal(left).setUvCoordinate(uv00).build(),
+ // Front
+ Vertex.builder().setPosition(p4).setNormal(front).setUvCoordinate(uv01).build(),
+ Vertex.builder().setPosition(p5).setNormal(front).setUvCoordinate(uv11).build(),
+ Vertex.builder().setPosition(p1).setNormal(front).setUvCoordinate(uv10).build(),
+ Vertex.builder().setPosition(p0).setNormal(front).setUvCoordinate(uv00).build(),
+ // Back
+ Vertex.builder().setPosition(p6).setNormal(back).setUvCoordinate(uv01).build(),
+ Vertex.builder().setPosition(p7).setNormal(back).setUvCoordinate(uv11).build(),
+ Vertex.builder().setPosition(p3).setNormal(back).setUvCoordinate(uv10).build(),
+ Vertex.builder().setPosition(p2).setNormal(back).setUvCoordinate(uv00).build(),
+ // Right
+ Vertex.builder().setPosition(p5).setNormal(right).setUvCoordinate(uv01).build(),
+ Vertex.builder().setPosition(p6).setNormal(right).setUvCoordinate(uv11).build(),
+ Vertex.builder().setPosition(p2).setNormal(right).setUvCoordinate(uv10).build(),
+ Vertex.builder().setPosition(p1).setNormal(right).setUvCoordinate(uv00).build(),
+ // Top
+ Vertex.builder().setPosition(p7).setNormal(up).setUvCoordinate(uv01).build(),
+ Vertex.builder().setPosition(p6).setNormal(up).setUvCoordinate(uv11).build(),
+ Vertex.builder().setPosition(p5).setNormal(up).setUvCoordinate(uv10).build(),
+ Vertex.builder().setPosition(p4).setNormal(up).setUvCoordinate(uv00).build()));
+
+ final int numSides = 6;
+ final int verticesPerSide = 4;
+ final int trianglesPerSide = 2;
+
+ ArrayList triangleIndices =
+ new ArrayList<>(numSides * trianglesPerSide * COORDS_PER_TRIANGLE);
+ for (int i = 0; i < numSides; i++) {
+ // First triangle for this side.
+ triangleIndices.add(3 + verticesPerSide * i);
+ triangleIndices.add(1 + verticesPerSide * i);
+ triangleIndices.add(0 + verticesPerSide * i);
+
+ // Second triangle for this side.
+ triangleIndices.add(3 + verticesPerSide * i);
+ triangleIndices.add(2 + verticesPerSide * i);
+ triangleIndices.add(1 + verticesPerSide * i);
+ }
+
+ Submesh submesh =
+ Submesh.builder().setTriangleIndices(triangleIndices).setMaterial(material).build();
+
+ RenderableDefinition renderableDefinition =
+ RenderableDefinition.builder()
+ .setVertices(vertices)
+ .setSubmeshes(Arrays.asList(submesh))
+ .build();
+
+ CompletableFuture future =
+ ModelRenderable.builder().setSource(renderableDefinition).build();
+
+ @Nullable ModelRenderable result;
+ try {
+ result = future.get();
+ } catch (ExecutionException | InterruptedException ex) {
+ throw new AssertionError("Error creating renderable.", ex);
+ }
+
+ if (result == null) {
+ throw new AssertionError("Error creating renderable.");
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates a {@link ModelRenderable} in the shape of a sphere with the give specifications.
+ *
+ * @param radius the radius of the constructed sphere
+ * @param center the center of the constructed sphere
+ * @param material the material to use for rendering the sphere
+ * @return renderable representing a sphere with the given parameters
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ // CompletableFuture requires api level 24
+ public static ModelRenderable makeSphere(float radius, Vector3 center, Material material) {
+ AndroidPreconditions.checkMinAndroidApiLevel();
+
+ final int stacks = 24;
+ final int slices = 24;
+
+ // Create Vertices.
+ ArrayList vertices = new ArrayList<>((slices + 1) * stacks + 2);
+ float pi = (float) Math.PI;
+ float doublePi = pi * 2.0f;
+
+ for (int stack = 0; stack <= stacks; stack++) {
+ float phi = pi * (float) stack / stacks;
+ float sinPhi = (float) Math.sin(phi);
+ float cosPhi = (float) Math.cos(phi);
+
+ for (int slice = 0; slice <= slices; slice++) {
+ float theta = doublePi * (float) (slice == slices ? 0 : slice) / slices;
+ float sinTheta = (float) Math.sin(theta);
+ float cosTheta = (float) Math.cos(theta);
+
+ Vector3 position = new Vector3(sinPhi * cosTheta, cosPhi, sinPhi * sinTheta).scaled(radius);
+ Vector3 normal = position.normalized();
+ position = Vector3.add(position, center);
+ Vertex.UvCoordinate uvCoordinate =
+ new Vertex.UvCoordinate(
+ 1.0f - ((float) slice / slices), 1.0f - ((float) stack / stacks));
+
+ Vertex vertex =
+ Vertex.builder()
+ .setPosition(position)
+ .setNormal(normal)
+ .setUvCoordinate(uvCoordinate)
+ .build();
+
+ vertices.add(vertex);
+ }
+ }
+
+ // Create triangles.
+ int numFaces = vertices.size();
+ int numTriangles = numFaces * 2;
+ int numIndices = numTriangles * 3;
+ ArrayList triangleIndices = new ArrayList<>(numIndices);
+
+ int v = 0;
+ for (int stack = 0; stack < stacks; stack++) {
+ for (int slice = 0; slice < slices; slice++) {
+ // Skip triangles at the caps that would have an area of zero.
+ boolean topCap = stack == 0;
+ boolean bottomCap = stack == stacks - 1;
+
+ int next = slice + 1;
+
+ if (!topCap) {
+ triangleIndices.add(v + slice);
+ triangleIndices.add(v + next);
+ triangleIndices.add(v + slice + slices + 1);
+ }
+
+ if (!bottomCap) {
+ triangleIndices.add(v + next);
+ triangleIndices.add(v + next + slices + 1);
+ triangleIndices.add(v + slice + slices + 1);
+ }
+ }
+ v += slices + 1;
+ }
+
+ Submesh submesh =
+ Submesh.builder().setTriangleIndices(triangleIndices).setMaterial(material).build();
+ RenderableDefinition renderableDefinition =
+ RenderableDefinition.builder()
+ .setVertices(vertices)
+ .setSubmeshes(Arrays.asList(submesh))
+ .build();
+
+ CompletableFuture future =
+ ModelRenderable.builder().setSource(renderableDefinition).build();
+
+ @Nullable ModelRenderable result;
+ try {
+ result = future.get();
+ } catch (ExecutionException | InterruptedException ex) {
+ throw new AssertionError("Error creating renderable.", ex);
+ }
+
+ if (result == null) {
+ throw new AssertionError("Error creating renderable.");
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates a {@link ModelRenderable} in the shape of a cylinder with the give specifications.
+ *
+ * @param radius the radius of the constructed cylinder
+ * @param height the height of the constructed cylinder
+ * @param center the center of the constructed cylinder
+ * @param material the material to use for rendering the cylinder
+ * @return renderable representing a cylinder with the given parameters
+ */
+ @SuppressWarnings("AndroidApiChecker")
+ // CompletableFuture requires api level 24
+ public static ModelRenderable makeCylinder(
+ float radius, float height, Vector3 center, Material material) {
+ AndroidPreconditions.checkMinAndroidApiLevel();
+
+ final int numberOfSides = 24;
+ final float halfHeight = height / 2;
+ final float thetaIncrement = (float) (2 * Math.PI) / numberOfSides;
+
+ float theta = 0;
+ float uStep = (float) 1.0 / numberOfSides;
+
+ ArrayList vertices = new ArrayList<>((numberOfSides + 1) * 4);
+ ArrayList lowerCapVertices = new ArrayList<>(numberOfSides + 1);
+ ArrayList upperCapVertices = new ArrayList<>(numberOfSides + 1);
+ ArrayList upperEdgeVertices = new ArrayList<>(numberOfSides + 1);
+
+ // Generate vertices along the sides of the cylinder.
+ for (int side = 0; side <= numberOfSides; side++) {
+ float cosTheta = (float) Math.cos(theta);
+ float sinTheta = (float) Math.sin(theta);
+
+ // Calculate edge vertices along bottom of cylinder
+ Vector3 lowerPosition = new Vector3(radius * cosTheta, -halfHeight, radius * sinTheta);
+ Vector3 normal = new Vector3(lowerPosition.x, 0, lowerPosition.z).normalized();
+ lowerPosition = Vector3.add(lowerPosition, center);
+ UvCoordinate uvCoordinate = new UvCoordinate(uStep * side, 0);
+
+ Vertex vertex =
+ Vertex.builder()
+ .setPosition(lowerPosition)
+ .setNormal(normal)
+ .setUvCoordinate(uvCoordinate)
+ .build();
+ vertices.add(vertex);
+
+ // Create a copy of lower vertex with bottom-facing normals for cap.
+ vertex =
+ Vertex.builder()
+ .setPosition(lowerPosition)
+ .setNormal(Vector3.down())
+ .setUvCoordinate(new UvCoordinate((cosTheta + 1f) / 2, (sinTheta + 1f) / 2))
+ .build();
+ lowerCapVertices.add(vertex);
+
+ // Calculate edge vertices along top of cylinder
+ Vector3 upperPosition = new Vector3(radius * cosTheta, halfHeight, radius * sinTheta);
+ normal = new Vector3(upperPosition.x, 0, upperPosition.z).normalized();
+ upperPosition = Vector3.add(upperPosition, center);
+ uvCoordinate = new UvCoordinate(uStep * side, 1);
+
+ vertex =
+ Vertex.builder()
+ .setPosition(upperPosition)
+ .setNormal(normal)
+ .setUvCoordinate(uvCoordinate)
+ .build();
+ upperEdgeVertices.add(vertex);
+
+ // Create a copy of upper vertex with up-facing normals for cap.
+ vertex =
+ Vertex.builder()
+ .setPosition(upperPosition)
+ .setNormal(Vector3.up())
+ .setUvCoordinate(new UvCoordinate((cosTheta + 1f) / 2, (sinTheta + 1f) / 2))
+ .build();
+ upperCapVertices.add(vertex);
+
+ theta += thetaIncrement;
+ }
+ vertices.addAll(upperEdgeVertices);
+
+ // Generate vertices for the centers of the caps of the cylinder.
+ final int lowerCenterIndex = vertices.size();
+ vertices.add(
+ Vertex.builder()
+ .setPosition(Vector3.add(center, new Vector3(0, -halfHeight, 0)))
+ .setNormal(Vector3.down())
+ .setUvCoordinate(new UvCoordinate(.5f, .5f))
+ .build());
+ vertices.addAll(lowerCapVertices);
+
+ final int upperCenterIndex = vertices.size();
+ vertices.add(
+ Vertex.builder()
+ .setPosition(Vector3.add(center, new Vector3(0, halfHeight, 0)))
+ .setNormal(Vector3.up())
+ .setUvCoordinate(new UvCoordinate(.5f, .5f))
+ .build());
+ vertices.addAll(upperCapVertices);
+
+ ArrayList triangleIndices = new ArrayList<>();
+
+ // Create triangles for each side
+ for (int side = 0; side < numberOfSides; side++) {
+ int bottomLeft = side;
+ int bottomRight = side + 1;
+ int topLeft = side + numberOfSides + 1;
+ int topRight = side + numberOfSides + 2;
+
+ // First triangle of side.
+ triangleIndices.add(bottomLeft);
+ triangleIndices.add(topRight);
+ triangleIndices.add(bottomRight);
+
+ // Second triangle of side.
+ triangleIndices.add(bottomLeft);
+ triangleIndices.add(topLeft);
+ triangleIndices.add(topRight);
+
+ // Add bottom cap triangle.
+ triangleIndices.add(lowerCenterIndex);
+ triangleIndices.add(lowerCenterIndex + side + 1);
+ triangleIndices.add(lowerCenterIndex + side + 2);
+
+ // Add top cap triangle.
+ triangleIndices.add(upperCenterIndex);
+ triangleIndices.add(upperCenterIndex + side + 2);
+ triangleIndices.add(upperCenterIndex + side + 1);
+ }
+
+ Submesh submesh =
+ Submesh.builder().setTriangleIndices(triangleIndices).setMaterial(material).build();
+
+ RenderableDefinition renderableDefinition =
+ RenderableDefinition.builder()
+ .setVertices(vertices)
+ .setSubmeshes(Arrays.asList(submesh))
+ .build();
+
+ CompletableFuture future =
+ ModelRenderable.builder().setSource(renderableDefinition).build();
+
+ @Nullable ModelRenderable result;
+ try {
+ result = future.get();
+ } catch (ExecutionException | InterruptedException ex) {
+ throw new AssertionError("Error creating renderable.", ex);
+ }
+
+ if (result == null) {
+ throw new AssertionError("Error creating renderable.");
+ }
+
+ return result;
+ }
+}
diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Texture.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Texture.java
new file mode 100644
index 0000000..bfefd9b
--- /dev/null
+++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Texture.java
@@ -0,0 +1,583 @@
+package com.google.ar.sceneform.rendering;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Build;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.android.filament.android.TextureHelper;
+
+import com.google.ar.core.annotations.UsedByNative;
+import com.google.ar.sceneform.resources.ResourceRegistry;
+import com.google.ar.sceneform.utilities.AndroidPreconditions;
+import com.google.ar.sceneform.utilities.LoadHelper;
+import com.google.ar.sceneform.utilities.Preconditions;
+import java.io.InputStream;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+
+/** Represents a reference to a texture. */
+@SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"}) // CompletableFuture
+@RequiresApi(api = Build.VERSION_CODES.N)
+@UsedByNative("material_java_wrappers.h")
+public class Texture {
+ private static final String TAG = Texture.class.getSimpleName();
+
+ /** Type of Texture usage. */
+ public enum Usage {
+ /** Texture contains a color map */
+ COLOR,
+ /** Assume color usage by default */
+ /** Texture contains a normal map */
+ NORMAL,
+ /** Texture contains arbitrary data */
+ DATA
+ }
+
+ // Set mipCount to the maximum number of levels, Filament will clamp it as required.
+ // This will make sure that all the mip levels are filled out, down to 1x1.
+ private static final int MIP_LEVELS_TO_GENERATE = 0xff;
+
+ @Nullable private final TextureInternalData textureData;
+
+ /** Constructs a default texture, if nothing else is set */
+ public static Builder builder() {
+ AndroidPreconditions.checkMinAndroidApiLevel();
+
+ return new Builder();
+ }
+
+ @SuppressWarnings({"initialization"})
+ @UsedByNative("material_java_wrappers.h")
+ private Texture(TextureInternalData textureData) {
+ this.textureData = textureData;
+ textureData.retain();
+ ResourceManager.getInstance()
+ .getTextureCleanupRegistry()
+ .register(this, new CleanupCallback(textureData));
+ }
+
+ Sampler getSampler() {
+ return Preconditions.checkNotNull(textureData).getSampler();
+ }
+
+ /**
+ * Get engine data required to use the texture.
+ *
+ * @hide
+ */
+ com.google.android.filament.Texture getFilamentTexture() {
+ return Preconditions.checkNotNull(textureData).getFilamentTexture();
+ }
+
+ private static com.google.android.filament.Texture.InternalFormat getInternalFormatForUsage(
+ Usage usage) {
+ com.google.android.filament.Texture.InternalFormat format;
+
+ switch (usage) {
+ case COLOR:
+ format = com.google.android.filament.Texture.InternalFormat.SRGB8_A8;
+ break;
+ case NORMAL:
+ case DATA:
+ default:
+ format = com.google.android.filament.Texture.InternalFormat.RGBA8;
+ break;
+ }
+ return format;
+ }
+
+ /** Factory class for {@link Texture} */
+ public static final class Builder {
+ /** The {@link Texture} will be constructed from the contents of this callable */
+ @Nullable private Callable inputStreamCreator = null;
+
+ @Nullable private Bitmap bitmap = null;
+ @Nullable private TextureInternalData textureInternalData = null;
+
+ private Usage usage = Usage.COLOR;
+ /** Enables reuse through the registry */
+ @Nullable private Object registryId = null;
+
+ private boolean inPremultiplied = true;
+
+ private Sampler sampler = Sampler.builder().build();
+
+ private static final int MAX_BITMAP_SIZE = 4096;
+
+ /** Constructor for asynchronous building. The sourceBuffer will be read later. */
+ private Builder() {}
+
+ /**
+ * Allows a {@link Texture} to be constructed from {@link Uri}. Construction will be
+ * asynchronous.
+ *
+ * @param sourceUri Sets a remote Uri or android resource Uri. The texture will be added to the
+ * registry using the Uri A previously registered texture with the same Uri will be re-used.
+ * @param context Sets the {@link Context} used to resolve sourceUri
+ * @return {@link Builder} for chaining setup calls.
+ */
+ public Builder setSource(Context context, Uri sourceUri) {
+ Preconditions.checkNotNull(sourceUri, "Parameter \"sourceUri\" was null.");
+
+ registryId = sourceUri;
+ setSource(LoadHelper.fromUri(context, sourceUri));
+ return this;
+ }
+
+ /**
+ * Allows a {@link Texture} to be constructed via callable function.
+ *
+ * @param inputStreamCreator Supplies an {@link InputStream} with the {@link Texture} data.
+ * @return {@link Builder} for chaining setup calls.
+ */
+ public Builder setSource(Callable inputStreamCreator) {
+ Preconditions.checkNotNull(inputStreamCreator, "Parameter \"inputStreamCreator\" was null.");
+
+ this.inputStreamCreator = inputStreamCreator;
+ bitmap = null;
+ return this;
+ }
+
+ /**
+ * Allows a {@link Texture} to be constructed from resource. Construction will be asynchronous.
+ *
+ * @param resource an android resource with raw type. A previously registered texture with the
+ * same resource id will be re-used.
+ * @param context {@link Context} used for resolution
+ * @return {@link Builder} for chaining setup calls.
+ */
+ public Builder setSource(Context context, int resource) {
+ setSource(LoadHelper.fromResource(context, resource));
+ registryId = context.getResources().getResourceName(resource);
+ return this;
+ }
+
+ /**
+ * Allows a {@link Texture} to be constructed from a {@link Bitmap}. Construction will be
+ * immediate.
+ *
+ * The Bitmap must meet the following conditions to be used by Sceneform:
+ *
+ *
+ * - {@link Bitmap#getConfig()} must be {@link Bitmap.Config#ARGB_8888}.
+ *
- {@link Bitmap#isPremultiplied()} must be true.
+ *
- The width and height must be smaller than 4096 pixels.
+ *
+ *
+ * @param bitmap {@link Bitmap} source of texture data
+ * @throws IllegalArgumentException if the bitmap isn't valid
+ */
+ public Builder setSource(Bitmap bitmap) {
+ Preconditions.checkNotNull(bitmap, "Parameter \"bitmap\" was null.");
+
+ if (bitmap.getConfig() != Bitmap.Config.ARGB_8888) {
+ throw new IllegalArgumentException(
+ "Invalid Bitmap: Bitmap's configuration must be "
+ + "ARGB_8888, but it was "
+ + bitmap.getConfig());
+ }
+
+ if (bitmap.hasAlpha() && !bitmap.isPremultiplied()) {
+ throw new IllegalArgumentException("Invalid Bitmap: Bitmap must be premultiplied.");
+ }
+
+ if (bitmap.getWidth() > MAX_BITMAP_SIZE || bitmap.getHeight() > MAX_BITMAP_SIZE) {
+ throw new IllegalArgumentException(
+ "Invalid Bitmap: Bitmap width and height must be "
+ + "smaller than 4096. Bitmap was "
+ + bitmap.getWidth()
+ + " width and "
+ + bitmap.getHeight()
+ + " height.");
+ }
+
+ this.bitmap = bitmap;
+ // TODO: don't overwrite calls to setRegistryId
+ registryId = null;
+ inputStreamCreator = null;
+ return this;
+ }
+
+ /**
+ * Sets internal data of the texture directly.
+ *
+ * @hide Hidden API direct from filament
+ */
+ public Builder setData(TextureInternalData textureInternalData) {
+ this.textureInternalData = textureInternalData;
+ return this;
+ }
+
+ /**
+ * Indicates whether the a texture loaded via an {@link InputStream}should be loaded with
+ * premultiplied alpha.
+ *
+ * @param inPremultiplied Whether the texture loaded via an {@link InputStream} should be loaded
+ * with premultiplied alpha. Default value is true.
+ * @return {@link Builder} for chaining setup calls.
+ */
+ Builder setPremultiplied(boolean inPremultiplied) {
+ this.inPremultiplied = inPremultiplied;
+ return this;
+ }
+
+ /**
+ * Allows a {@link Texture} to be reused. If registryId is non-null it will be saved in a
+ * registry and the registry will be checked for this id before construction.
+ *
+ * @param registryId Allows the function to be skipped and a previous texture to be re-used.
+ * @return {@link Builder} for chaining setup calls.
+ */
+ public Builder setRegistryId(Object registryId) {
+ this.registryId = registryId;
+ return this;
+ }
+
+ /**
+ * Mark the {@link Texture} as a containing color, normal or arbitrary data. Color is the
+ * default.
+ *
+ * @param usage Sets the kind of data in {@link Texture}
+ * @return {@link Builder} for chaining setup calls.
+ */
+ public Builder setUsage(Usage usage) {
+ this.usage = usage;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Sampler}to control rendering parameters on the {@link Texture}.
+ *
+ * @param sampler Controls appearance of the {@link Texture}
+ * @return {@link Builder} for chaining setup calls.
+ */
+ public Builder setSampler(Sampler sampler) {
+ this.sampler = sampler;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link Texture} based on the parameters set previously
+ *
+ * @throws IllegalStateException if the builder is not properly set
+ */
+ public CompletableFuture build() {
+ AndroidPreconditions.checkUiThread();
+ Object registryId = this.registryId;
+ if (registryId != null) {
+ // See if a texture has already been registered by this id, if so re-use it.
+ ResourceRegistry registry = ResourceManager.getInstance().getTextureRegistry();
+ @Nullable CompletableFuture textureFuture = registry.get(registryId);
+ if (textureFuture != null) {
+ return textureFuture;
+ }
+ }
+
+ if (textureInternalData != null && registryId != null) {
+ throw new IllegalStateException("Builder must not set both a bitmap and filament texture");
+ }
+
+ CompletableFuture result;
+ if (this.textureInternalData != null) {
+ result = CompletableFuture.completedFuture(new Texture(this.textureInternalData));
+ } else {
+ CompletableFuture