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: + * + *

+ * + * 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: + *

+ */ + 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: + * + */ + 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: + * + *

+ */ + // 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: + * + *

+ * + * An active Node has the following behavior: + * + * + * + * @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: + * + *

+ * + * 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> 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 bitmapFuture; + if (inputStreamCreator != null) { + bitmapFuture = makeBitmap(inputStreamCreator, inPremultiplied); + } else if (bitmap != null) { + bitmapFuture = CompletableFuture.completedFuture(bitmap); + } else { + throw new IllegalStateException("Texture must have a source."); + } + + result = + bitmapFuture.thenApplyAsync( + loadedBitmap -> { + TextureInternalData textureData = + makeTextureData(loadedBitmap, sampler, usage, MIP_LEVELS_TO_GENERATE); + return new Texture(textureData); + }, + ThreadPools.getMainExecutor()); + } + + if (registryId != null) { + ResourceRegistry registry = ResourceManager.getInstance().getTextureRegistry(); + registry.register(registryId, result); + } + + FutureHelper.logOnException( + TAG, result, "Unable to load Texture registryId='" + registryId + "'"); + return result; + } + + private static CompletableFuture makeBitmap( + Callable inputStreamCreator, boolean inPremultiplied) { + return CompletableFuture.supplyAsync( + () -> { + // Read the texture file. + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; + options.inPremultiplied = inPremultiplied; + Bitmap bitmap; + + // Open and read the texture file. + try (InputStream inputStream = inputStreamCreator.call()) { + bitmap = BitmapFactory.decodeStream(inputStream, null, options); + } catch (Exception e) { + throw new IllegalStateException(e); + } + + if (bitmap == null) { + throw new IllegalStateException( + "Failed to decode the texture bitmap. The InputStream was not a valid bitmap."); + } + + if (bitmap.getConfig() != Bitmap.Config.ARGB_8888) { + throw new IllegalStateException("Texture must use ARGB8 format."); + } + + return bitmap; + }, + ThreadPools.getThreadPoolExecutor()); + } + + private static TextureInternalData makeTextureData( + Bitmap bitmap, Sampler sampler, Usage usage, int mipLevels) { + IEngine engine = EngineInstance.getEngine(); + + // Due to fun ambiguities between Texture (RenderCore) and Texture (Filament) + // Texture references must be fully qualified giving rise to the following monstrosity + // of verbosity. + final com.google.android.filament.Texture.InternalFormat textureInternalFormat = + getInternalFormatForUsage(usage); + final com.google.android.filament.Texture.Sampler textureSampler = + com.google.android.filament.Texture.Sampler.SAMPLER_2D; + + com.google.android.filament.Texture filamentTexture = + new com.google.android.filament.Texture.Builder() + .width(bitmap.getWidth()) + .height(bitmap.getHeight()) + .depth(1) + .levels(mipLevels) + .sampler(textureSampler) + .format(textureInternalFormat) + .build(engine.getFilamentEngine()); + + TextureHelper.setBitmap(engine.getFilamentEngine(), filamentTexture, 0, bitmap); + + if (mipLevels > 1) { + filamentTexture.generateMipmaps(engine.getFilamentEngine()); + } + + return new TextureInternalData(filamentTexture, sampler); + } + } + + // LINT.IfChange(api) + /** Controls what settings are used to sample Textures when rendering. */ + @UsedByNative("material_java_wrappers.h") + public static class Sampler { + /** Options for Minification Filter function. */ + @UsedByNative("material_java_wrappers.h") + public enum MinFilter { + @UsedByNative("material_java_wrappers.h") + NEAREST, + @UsedByNative("material_java_wrappers.h") + LINEAR, + @UsedByNative("material_java_wrappers.h") + NEAREST_MIPMAP_NEAREST, + @UsedByNative("material_java_wrappers.h") + LINEAR_MIPMAP_NEAREST, + @UsedByNative("material_java_wrappers.h") + NEAREST_MIPMAP_LINEAR, + @UsedByNative("material_java_wrappers.h") + LINEAR_MIPMAP_LINEAR + } + + /** Options for Magnification Filter function. */ + @UsedByNative("material_java_wrappers.h") + public enum MagFilter { + @UsedByNative("material_java_wrappers.h") + NEAREST, + @UsedByNative("material_java_wrappers.h") + LINEAR + } + + /** Options for Wrap Mode function. */ + @UsedByNative("material_java_wrappers.h") + public enum WrapMode { + @UsedByNative("material_java_wrappers.h") + CLAMP_TO_EDGE, + @UsedByNative("material_java_wrappers.h") + REPEAT, + @UsedByNative("material_java_wrappers.h") + MIRRORED_REPEAT + } + + private final MinFilter minFilter; + private final MagFilter magFilter; + private final WrapMode wrapModeS; + private final WrapMode wrapModeT; + private final WrapMode wrapModeR; + + + + + + + + + + + + private Sampler(Sampler.Builder builder) { + this.minFilter = builder.minFilter; + this.magFilter = builder.magFilter; + this.wrapModeS = builder.wrapModeS; + this.wrapModeT = builder.wrapModeT; + this.wrapModeR = builder.wrapModeR; + } + + /** + * Get the minifying function used whenever the level-of-detail function determines that the + * texture should be minified. + */ + public MinFilter getMinFilter() { + return minFilter; + } + + /** + * Get the magnification function used whenever the level-of-detail function determines that the + * texture should be magnified. + */ + public MagFilter getMagFilter() { + return magFilter; + } + + /** + * Get the wrap mode for texture coordinate S. The wrap mode determines how a texture is + * rendered for uv coordinates outside the range of [0, 1]. + */ + public WrapMode getWrapModeS() { + return wrapModeS; + } + + /** + * Get the wrap mode for texture coordinate T. The wrap mode determines how a texture is + * rendered for uv coordinates outside the range of [0, 1]. + */ + public WrapMode getWrapModeT() { + return wrapModeT; + } + + /** + * Get the wrap mode for texture coordinate R. The wrap mode determines how a texture is + * rendered for uv coordinates outside the range of [0, 1]. + */ + public WrapMode getWrapModeR() { + return wrapModeR; + } + + public static Builder builder() { + return new Sampler.Builder() + .setMinFilter(MinFilter.LINEAR_MIPMAP_LINEAR) + .setMagFilter(MagFilter.LINEAR) + .setWrapMode(WrapMode.CLAMP_TO_EDGE); + } + + /** Builder for constructing Sampler objects. */ + public static class Builder { + private MinFilter minFilter; + private MagFilter magFilter; + private WrapMode wrapModeS; + private WrapMode wrapModeT; + private WrapMode wrapModeR; + + /** Set both the texture minifying function and magnification function. */ + Builder setMinMagFilter(MagFilter minMagFilter) { + return setMinFilter(MinFilter.values()[minMagFilter.ordinal()]).setMagFilter(minMagFilter); + } + + /** + * Set the minifying function used whenever the level-of-detail function determines that the + * texture should be minified. + */ + public Builder setMinFilter(MinFilter minFilter) { + this.minFilter = minFilter; + return this; + } + + /** + * Set the magnification function used whenever the level-of-detail function determines that + * the texture should be magnified. + */ + public Builder setMagFilter(MagFilter magFilter) { + this.magFilter = magFilter; + return this; + } + + /** + * Set the wrap mode for all texture coordinates. The wrap mode determines how a texture is + * rendered for uv coordinates outside the range of [0, 1]. + */ + public Builder setWrapMode(WrapMode wrapMode) { + return setWrapModeS(wrapMode).setWrapModeT(wrapMode).setWrapModeR(wrapMode); + } + + /** + * Set the wrap mode for texture coordinate S. The wrap mode determines how a texture is + * rendered for uv coordinates outside the range of [0, 1]. + */ + public Builder setWrapModeS(WrapMode wrapMode) { + wrapModeS = wrapMode; + return this; + } + + /** + * Set the wrap mode for texture coordinate T. The wrap mode determines how a texture is + * rendered for uv coordinates outside the range of [0, 1]. + */ + public Builder setWrapModeT(WrapMode wrapMode) { + wrapModeT = wrapMode; + return this; + } + + /** + * Set the wrap mode for texture coordinate R. The wrap mode determines how a texture is + * rendered for uv coordinates outside the range of [0, 1]. + */ + public Builder setWrapModeR(WrapMode wrapMode) { + wrapModeR = wrapMode; + return this; + } + + /** Construct a Sampler from the properties of the Builder. */ + public Sampler build() { + return new Sampler(this); + } + } + } + // LINT.ThenChange( + // //depot/google3/third_party/arcore/ar/sceneform/loader/model/material_java_wrappers.h:api + // ) + + /** Cleanup {@link TextureInternalData} after garbage collection */ + private static final class CleanupCallback implements Runnable { + private final TextureInternalData textureData; + + CleanupCallback(TextureInternalData textureData) { + this.textureData = textureData; + } + + @Override + public void run() { + AndroidPreconditions.checkUiThread(); + if (textureData != null) { + textureData.release(); + } + } + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/TextureInternalData.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/TextureInternalData.java new file mode 100644 index 0000000..43e07ab --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/TextureInternalData.java @@ -0,0 +1,50 @@ +package com.google.ar.sceneform.rendering; + +import androidx.annotation.Nullable; +import com.google.ar.core.annotations.UsedByNative; +import com.google.ar.sceneform.resources.SharedReference; +import com.google.ar.sceneform.utilities.AndroidPreconditions; + +/** + * Represents shared data used by {@link Texture}s for rendering. The data will be released when all + * {@link Texture}s using this data are finalized. + * + * @hide Only for use for private features such as occlusion. + */ +@UsedByNative("material_java_wrappers.h") +public class TextureInternalData extends SharedReference { + @Nullable private com.google.android.filament.Texture filamentTexture; + + private final Texture.Sampler sampler; + + @UsedByNative("material_java_wrappers.h") + public TextureInternalData( + com.google.android.filament.Texture filamentTexture, Texture.Sampler sampler) { + this.filamentTexture = filamentTexture; + this.sampler = sampler; + } + + com.google.android.filament.Texture getFilamentTexture() { + if (filamentTexture == null) { + throw new IllegalStateException("Filament Texture is null."); + } + + return filamentTexture; + } + + Texture.Sampler getSampler() { + return sampler; + } + + @Override + protected void onDispose() { + AndroidPreconditions.checkUiThread(); + + IEngine engine = EngineInstance.getEngine(); + com.google.android.filament.Texture filamentTexture = this.filamentTexture; + this.filamentTexture = null; + if (filamentTexture != null && engine != null && engine.isValid()) { + engine.destroyTexture(filamentTexture); + } + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ThreadPools.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ThreadPools.java new file mode 100644 index 0000000..4b11d4a --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ThreadPools.java @@ -0,0 +1,56 @@ +package com.google.ar.sceneform.rendering; + +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import java.util.concurrent.Executor; + +/** + * Provides access to default {@link Executor}s to be used + * + * @hide + */ +public class ThreadPools { + private static Executor mainExecutor; + private static Executor threadPoolExecutor; + + private ThreadPools() {} + + /** {@link Executor} for anything that that touches {@link Renderer} state */ + public static Executor getMainExecutor() { + if (mainExecutor == null) { + mainExecutor = + new Executor() { + private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable runnable) { + handler.post(runnable); + } + }; + } + return mainExecutor; + } + + /** @param executor provides access to the main thread. */ + public static void setMainExecutor(Executor executor) { + mainExecutor = executor; + } + + /** Default background {@link Executor} for async operations including file reading. */ + public static Executor getThreadPoolExecutor() { + if (threadPoolExecutor == null) { + return AsyncTask.THREAD_POOL_EXECUTOR; + } + return threadPoolExecutor; + } + + /** + * Sets the default background {@link Executor}. + * + *

Tasks may be long running. This should not include the main thread + */ + public static void setThreadPoolExecutor(Executor executor) { + threadPoolExecutor = executor; + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Vertex.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Vertex.java new file mode 100644 index 0000000..8f21e72 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/Vertex.java @@ -0,0 +1,113 @@ +package com.google.ar.sceneform.rendering; + +import androidx.annotation.Nullable; +import com.google.ar.sceneform.math.Vector3; + +/** + * Represents a Vertex for a {@link RenderableDefinition}. Used for constructing renderables + * dynamically. + * + * @see ModelRenderable.Builder + * @see ViewRenderable.Builder + */ +public class Vertex { + /** Represents a texture Coordinate for a Vertex. Values should be between 0 and 1. */ + public static final class UvCoordinate { + public float x; + public float y; + + public UvCoordinate(float x, float y) { + this.x = x; + this.y = y; + } + } + + // Required. + private final Vector3 position = Vector3.zero(); + + // Optional. + @Nullable private Vector3 normal; + @Nullable private UvCoordinate uvCoordinate; + @Nullable private Color color; + + public void setPosition(Vector3 position) { + this.position.set(position); + } + + public Vector3 getPosition() { + return position; + } + + public void setNormal(@Nullable Vector3 normal) { + this.normal = normal; + } + + @Nullable + public Vector3 getNormal() { + return normal; + } + + public void setUvCoordinate(@Nullable UvCoordinate uvCoordinate) { + this.uvCoordinate = uvCoordinate; + } + + @Nullable + public UvCoordinate getUvCoordinate() { + return uvCoordinate; + } + + public void setColor(@Nullable Color color) { + this.color = color; + } + + @Nullable + public Color getColor() { + return color; + } + + private Vertex(Builder builder) { + position.set(builder.position); + normal = builder.normal; + uvCoordinate = builder.uvCoordinate; + color = builder.color; + } + + public static Builder builder() { + return new Builder(); + } + + /** Factory class for {@link Vertex}. */ + public static final class Builder { + // Required. + private final Vector3 position = Vector3.zero(); + + // Optional. + @Nullable private Vector3 normal; + @Nullable private UvCoordinate uvCoordinate; + @Nullable private Color color; + + public Builder setPosition(Vector3 position) { + this.position.set(position); + return this; + } + + public Builder setNormal(@Nullable Vector3 normal) { + this.normal = normal; + return this; + } + + public Builder setUvCoordinate(@Nullable UvCoordinate uvCoordinate) { + this.uvCoordinate = uvCoordinate; + return this; + } + + public Builder setColor(@Nullable Color color) { + this.color = color; + return this; + } + + public Vertex build() { + return new Vertex(this); + } + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewAttachmentManager.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewAttachmentManager.java new file mode 100644 index 0000000..5b4f4d4 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewAttachmentManager.java @@ -0,0 +1,129 @@ +package com.google.ar.sceneform.rendering; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +/** + * Manages a {@link FrameLayout} that is attached directly to a {@link WindowManager} that other + * views can be added and removed from. + * + *

To render a {@link View}, the {@link View} must be attached to a {@link WindowManager} so that + * it can be properly drawn. This class encapsulates a {@link FrameLayout} that is attached to a + * {@link WindowManager} that other views can be added to as children. This allows us to safely and + * correctly draw the {@link View} associated with {@link ViewRenderable}'s while keeping them + * isolated from the rest of the activities View hierarchy. + * + *

Additionally, this manages the lifecycle of the window to help ensure that the window is + * added/removed from the WindowManager at the appropriate times. + * + * @hide + */ +// TODO: Create Unit Tests for this class. +class ViewAttachmentManager { + // View that owns the ViewAttachmentManager. + // Used to post callbacks onto the UI thread. + private final View ownerView; + + private final WindowManager windowManager; + private final WindowManager.LayoutParams windowLayoutParams; + + private final FrameLayout frameLayout; + private final ViewGroup.LayoutParams viewLayoutParams; + + private static final String VIEW_RENDERABLE_WINDOW = "ViewRenderableWindow"; + + /** + * Construct a ViewAttachmentManager. + * + * @param ownerView used by the ViewAttachmentManager to post callbacks on the UI thread + */ + ViewAttachmentManager(Context context, View ownerView) { + this.ownerView = ownerView; + + windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + windowLayoutParams = createWindowLayoutParams(); + + frameLayout = new FrameLayout(context); + viewLayoutParams = createViewLayoutParams(); + } + + FrameLayout getFrameLayout() { + return frameLayout; + } + + void onResume() { + // A ownerView can only be added to the WindowManager after the activity has finished resuming. + // Therefore, we must use post to ensure that the window is only added after resume is finished. + ownerView.post( + () -> { + if (frameLayout.getParent() == null && ownerView.isAttachedToWindow()) { + windowManager.addView(frameLayout, windowLayoutParams); + } + }); + } + + void onPause() { + // The ownerView must be removed from the WindowManager before the activity is destroyed, or the + // window will be leaked. Therefore we add/remove the ownerView in resume/pause. + if (frameLayout.getParent() != null) { + windowManager.removeView(frameLayout); + } + } + + /** + * Add a ownerView as a child of the {@link FrameLayout} that is attached to the {@link + * WindowManager}. + * + *

Used by {@link RenderViewToExternalTexture} to ensure that the ownerView is drawn with all + * appropriate lifecycle events being called correctly. + */ + void addView(View view) { + if (view.getParent() == frameLayout) { + return; + } + + frameLayout.addView(view, viewLayoutParams); + } + + /** + * Remove a ownerView from the {@link FrameLayout} that is attached to the {@link WindowManager}. + * + *

Used by {@link RenderViewToExternalTexture} to remove ownerView's that no longer need to be + * drawn. + */ + void removeView(View view) { + if (view.getParent() != frameLayout) { + return; + } + + frameLayout.removeView(view); + } + + private static WindowManager.LayoutParams createWindowLayoutParams() { + WindowManager.LayoutParams params = + new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + PixelFormat.TRANSLUCENT); + params.setTitle(VIEW_RENDERABLE_WINDOW); + + return params; + } + + private static ViewGroup.LayoutParams createViewLayoutParams() { + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + return params; + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewRenderable.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewRenderable.java new file mode 100644 index 0000000..52ff925 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewRenderable.java @@ -0,0 +1,551 @@ +package com.google.ar.sceneform.rendering; + +import android.content.Context; +import android.os.Build; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +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.resources.ResourceRegistry; +import com.google.ar.sceneform.utilities.AndroidPreconditions; +import com.google.ar.sceneform.utilities.Preconditions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.OptionalInt; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +/** + * Renders a 2D Android view in 3D space by attaching it to a {@link com.google.ar.sceneform.Node} + * with {@link com.google.ar.sceneform.Node#setRenderable(Renderable)}. By default, the size of the + * view is 1 meter in the {@link com.google.ar.sceneform.Scene} per 250dp in the layout. Use a + * {@link ViewSizer} to control how the size of the view in the {@link + * com.google.ar.sceneform.Scene} is calculated. + * + *

{@code
+ * future = ViewRenderable.builder().setView(context, R.layout.view).build();
+ * viewRenderable = future.thenAccept(...);
+ * }
+ */ +@RequiresApi(api = Build.VERSION_CODES.N) + +public class ViewRenderable extends Renderable { + private static final String TAG = ViewRenderable.class.getSimpleName(); + + /** + * Controls the horizontal alignment of the {@link ViewRenderable} relative to the {@link + * com.google.ar.sceneform.Node} it is attached to. The default value is CENTER. + */ + public enum HorizontalAlignment { + LEFT, + CENTER, + RIGHT + } + + /** + * Controls the vertical alignment of the {@link ViewRenderable} relative to the {@link + * com.google.ar.sceneform.Node} it is attached to. The default value is BOTTOM. + */ + public enum VerticalAlignment { + BOTTOM, + CENTER, + TOP + } + + @Nullable private ViewRenderableInternalData viewRenderableData; + private final View view; + + // Used to apply a final scale to the renderable that makes it render at an appropriate size based + // on the size of the view. + private final Matrix viewScaleMatrix = new Matrix(); + + private ViewSizer viewSizer; + private VerticalAlignment verticalAlignment = VerticalAlignment.BOTTOM; + private HorizontalAlignment horizontalAlignment = HorizontalAlignment.CENTER; + + @Nullable private Renderer renderer; + private boolean isInitialized; + + @SuppressWarnings({"initialization"}) + private final RenderViewToExternalTexture.OnViewSizeChangedListener onViewSizeChangedListener = + (int width, int height) -> { + if (isInitialized) { + updateSuggestedCollisionShapeAsync(); + } + }; + + /** The 2D Android {@link View} that is rendered by this {@link ViewRenderable}. */ + public View getView() { + return view; + } + + /** + * Creates a new instance of this ViewRenderable. + * + *

The new renderable will have unique copy of all mutable state. All materials referenced by + * the ViewRenderable will also be instanced. Immutable data will be shared between the instances. + * The new ViewRenderable will reference the same getFilamentEngine View as the original + * ViewRenderable. + */ + @Override + public ViewRenderable makeCopy() { + return new ViewRenderable(this); + } + + /** @hide */ + @SuppressWarnings({"initialization"}) + // Suppress @UnderInitialization warning. + ViewRenderable(Builder builder, View view) { + super(builder); + + Preconditions.checkNotNull(view, "Parameter \"view\" was null."); + + this.view = view; + viewSizer = builder.viewSizer; + horizontalAlignment = builder.horizontalAlignment; + verticalAlignment = builder.verticalAlignment; + RenderViewToExternalTexture renderView = + new RenderViewToExternalTexture(view.getContext(), view); + renderView.addOnViewSizeChangedListener(onViewSizeChangedListener); + viewRenderableData = new ViewRenderableInternalData(renderView); + viewRenderableData.retain(); + + // Empty collision box. Will be modified to fit the size of the view after the view is measured. + // If the size of the view changes, the collision shape will change too. + collisionShape = new Box(Vector3.zero()); + } + + ViewRenderable(ViewRenderable other) { + super(other); + + view = other.view; + viewSizer = other.viewSizer; + horizontalAlignment = other.horizontalAlignment; + verticalAlignment = other.verticalAlignment; + viewRenderableData = Preconditions.checkNotNull(other.viewRenderableData); + viewRenderableData.retain(); + viewRenderableData.getRenderView().addOnViewSizeChangedListener(onViewSizeChangedListener); + } + + /** + * Gets the {@link ViewSizer} that controls the size of this {@link ViewRenderable} in the {@link + * com.google.ar.sceneform.Scene}. + */ + public ViewSizer getSizer() { + return viewSizer; + } + + /** + * Sets the {@link ViewSizer} that controls the size of this {@link ViewRenderable} in the {@link + * com.google.ar.sceneform.Scene}. + */ + public void setSizer(ViewSizer viewSizer) { + Preconditions.checkNotNull(viewSizer, "Parameter \"viewSizer\" was null."); + this.viewSizer = viewSizer; + updateSuggestedCollisionShape(); + } + + /** + * Gets the {@link HorizontalAlignment} that controls where the {@link ViewRenderable} is + * positioned relative to the {@link com.google.ar.sceneform.Node} it is attached to along the + * x-axis. The default is {@link HorizontalAlignment#CENTER}. + */ + public HorizontalAlignment getHorizontalAlignment() { + return horizontalAlignment; + } + + /** + * Sets the {@link HorizontalAlignment} that controls where the {@link ViewRenderable} is + * positioned relative to the {@link com.google.ar.sceneform.Node} it is attached to along the + * x-axis. The default is {@link HorizontalAlignment#CENTER}. + */ + public void setHorizontalAlignment(HorizontalAlignment horizontalAlignment) { + this.horizontalAlignment = horizontalAlignment; + updateSuggestedCollisionShape(); + } + + /** + * Gets the {@link VerticalAlignment} that controls where the {@link ViewRenderable} is positioned + * relative to the {@link com.google.ar.sceneform.Node} it is attached to along the y-axis. The + * default is {@link VerticalAlignment#BOTTOM}. + */ + public VerticalAlignment getVerticalAlignment() { + return verticalAlignment; + } + + /** + * Sets the {@link VerticalAlignment} that controls where the {@link ViewRenderable} is positioned + * relative to the {@link com.google.ar.sceneform.Node} it is attached to along the y-axis. The + * default is {@link VerticalAlignment#BOTTOM}. + */ + public void setVerticalAlignment(VerticalAlignment verticalAlignment) { + this.verticalAlignment = verticalAlignment; + updateSuggestedCollisionShape(); + } + + /** + * Takes the model matrix from the {@link TransformProvider} for rendering this {@link + * com.google.ar.sceneform.Node} and scales it to size it appropriately based on the meters to + * pixel ratio for the view. + * + * @hide + * @param originalMatrix + */ + @Override + public Matrix getFinalModelMatrix(final Matrix originalMatrix) { + Preconditions.checkNotNull(originalMatrix, "Parameter \"originalMatrix\" was null."); + // May be better to cache this when the transform provider's model matrix changes. + // This would require saving the matrix on a per-instance basis instead of a per-renderable + // basis as well. + + Vector3 size = viewSizer.getSize(view); + viewScaleMatrix.makeScale(new Vector3(size.x, size.y, 1.0f)); + + // Set the translation of the matrix based on the alignment pre-scaled by the size. + // This is much more efficient than allocating an additional matrix and doing a matrix multiply. + viewScaleMatrix.setTranslation( + new Vector3( + getOffsetRatioForAlignment(horizontalAlignment) * size.x, + getOffsetRatioForAlignment(verticalAlignment) * size.y, + 0.0f)); + + Matrix.multiply(originalMatrix, viewScaleMatrix, viewScaleMatrix); + + return viewScaleMatrix; + } + + /** @hide */ + @Override + @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"}) // CompletableFuture + void prepareForDraw() { + if (getId().isEmpty()) { + return; + } + + ViewRenderableInternalData data = Preconditions.checkNotNull(viewRenderableData); + RenderViewToExternalTexture renderViewToExternalTexture = data.getRenderView(); + + if (!renderViewToExternalTexture.isAttachedToWindow() + || !renderViewToExternalTexture.isLaidOut()) { + // Wait for the view to finish attachment. + return; + } + + // Wait until one frame after the surface texture has been drawn to for the first time. + // Fixes an issue where the ViewRenderable would render black for a frame before displaying. + boolean hasDrawnToSurfaceTexture = renderViewToExternalTexture.hasDrawnToSurfaceTexture(); + if (!hasDrawnToSurfaceTexture) { + return; + } + + if (!isInitialized) { + getMaterial() + .setExternalTexture("viewTexture", renderViewToExternalTexture.getExternalTexture()); + updateSuggestedCollisionShape(); + + isInitialized = true; + } + + if (renderer != null && renderer.isFrontFaceWindingInverted()) { + getMaterial().setFloat2("offsetUv", 1, 0); + } + + super.prepareForDraw(); + } + + @Override + void attachToRenderer(Renderer renderer) { + Preconditions.checkNotNull(viewRenderableData) + .getRenderView() + .attachView(renderer.getViewAttachmentManager()); + this.renderer = renderer; + } + + @Override + void detatchFromRenderer() { + Preconditions.checkNotNull(viewRenderableData).getRenderView().detachView(); + this.renderer = null; + } + + private void updateSuggestedCollisionShapeAsync() { + view.post(this::updateSuggestedCollisionShape); + } + + private void updateSuggestedCollisionShape() { + if (getId().isEmpty()) { + return; + } + + Box box = (Box) collisionShape; + if (box == null) { + return; + } + + IRenderableInternalData renderableData = getRenderableData(); + Vector3 viewSize = viewSizer.getSize(view); + + Vector3 size = renderableData.getSizeAabb(); + size.x *= viewSize.x; + size.y *= viewSize.y; + + Vector3 center = renderableData.getCenterAabb(); + center.x *= viewSize.x; + center.y *= viewSize.y; + + // Offset the collision shape based on the alignment. + center.x += getOffsetRatioForAlignment(horizontalAlignment) * size.x; + center.y += getOffsetRatioForAlignment(verticalAlignment) * size.y; + + box.setSize(size); + box.setCenter(center); + } + + private float getOffsetRatioForAlignment(HorizontalAlignment horizontalAlignment) { + IRenderableInternalData data = getRenderableData(); + Vector3 centerAabb = data.getCenterAabb(); + Vector3 extentsAabb = data.getExtentsAabb(); + + switch (horizontalAlignment) { + case LEFT: + return -centerAabb.x + extentsAabb.x; + case CENTER: + return -centerAabb.x; + case RIGHT: + return -centerAabb.x - extentsAabb.x; + } + throw new IllegalStateException("Invalid HorizontalAlignment: " + horizontalAlignment); + } + + private float getOffsetRatioForAlignment(VerticalAlignment verticalAlignment) { + IRenderableInternalData data = getRenderableData(); + Vector3 centerAabb = data.getCenterAabb(); + Vector3 extentsAabb = data.getExtentsAabb(); + + switch (verticalAlignment) { + case BOTTOM: + return -centerAabb.y + extentsAabb.y; + case CENTER: + return -centerAabb.y; + case TOP: + return -centerAabb.y - extentsAabb.y; + } + throw new IllegalStateException("Invalid VerticalAlignment: " + verticalAlignment); + } + + /** @hide */ + @Override + protected void finalize() throws Throwable { + try { + ThreadPools.getMainExecutor().execute(() -> dispose()); + } catch (Exception e) { + Log.e(TAG, "Error while Finalizing View Renderable.", e); + } finally { + super.finalize(); + } + } + + /** @hide */ + void dispose() { + AndroidPreconditions.checkUiThread(); + + ViewRenderableInternalData viewRenderableData = this.viewRenderableData; + if (viewRenderableData != null) { + viewRenderableData.getRenderView().removeOnViewSizeChangedListener(onViewSizeChangedListener); + viewRenderableData.release(); + this.viewRenderableData = null; + } + } + + /** Constructs a {@link ViewRenderable} */ + public static Builder builder() { + AndroidPreconditions.checkMinAndroidApiLevel(); + return new Builder(); + } + + /** Factory class for {@link ViewRenderable} */ + public static final class Builder extends Renderable.Builder { + private static final int DEFAULT_DP_TO_METERS = 250; + @Nullable private View view; + private ViewSizer viewSizer = new DpToMetersViewSizer(DEFAULT_DP_TO_METERS); + private VerticalAlignment verticalAlignment = VerticalAlignment.BOTTOM; + private HorizontalAlignment horizontalAlignment = HorizontalAlignment.CENTER; + + @SuppressWarnings("AndroidApiChecker") + private OptionalInt resourceId = OptionalInt.empty(); + + private Builder() {} + + public Builder setView(Context context, View view) { + this.view = view; + this.context = context; + registryId = view; + return this; + } + + @SuppressWarnings("AndroidApiChecker") + public Builder setView(Context context, int resourceId) { + this.resourceId = OptionalInt.of(resourceId); + this.context = context; + registryId = null; + return this; + } + + /** + * Set the {@link ViewSizer} that controls the size of the built {@link ViewRenderable} in the + * {@link com.google.ar.sceneform.Scene}. + */ + public Builder setSizer(ViewSizer viewSizer) { + Preconditions.checkNotNull(viewSizer, "Parameter \"viewSizer\" was null."); + this.viewSizer = viewSizer; + return this; + } + + /** + * Sets the {@link HorizontalAlignment} that controls where the {@link ViewRenderable} is + * positioned relative to the {@link com.google.ar.sceneform.Node} it is attached to along the + * x-axis. The default is {@link HorizontalAlignment#CENTER}. + */ + public Builder setHorizontalAlignment(HorizontalAlignment horizontalAlignment) { + this.horizontalAlignment = horizontalAlignment; + return this; + } + + /** + * Sets the {@link VerticalAlignment} that controls where the {@link ViewRenderable} is + * positioned relative to the {@link com.google.ar.sceneform.Node} it is attached to along the + * y-axis. The default is {@link VerticalAlignment#BOTTOM}. + */ + public Builder setVerticalAlignment(VerticalAlignment verticalAlignment) { + this.verticalAlignment = verticalAlignment; + return this; + } + + @Override + @SuppressWarnings("AndroidApiChecker") // java.util.concurrent.CompletableFuture + public CompletableFuture build() { + if (!hasSource() && context != null) { + // For ViewRenderables, the registryId must come from the View, not the RCB source. + // If the source is a View, use that as the registryId. If the view is null, then the source + // is a resource id and the registryId should also be null. + registryId = view; + + CompletableFuture setSourceFuture = Material.builder() + .setSource( + context, + RenderingResources.GetSceneformResource( + context, RenderingResources.Resource.VIEW_RENDERABLE_MATERIAL)) + .build() + .thenAccept( + material -> { + + ArrayList vertices = new ArrayList<>(); + vertices.add(Vertex.builder() + .setPosition(new Vector3(-0.5f, 0.0f, 0.0f)) + .setNormal(new Vector3(0.0f, 0.0f, 1.0f)) + .setUvCoordinate(new Vertex.UvCoordinate(0.0f, 0.0f)) + .build()); + vertices.add(Vertex.builder() + .setPosition(new Vector3(0.5f, 0.0f, 0.0f)) + .setNormal(new Vector3(0.0f, 0.0f, 1.0f)) + .setUvCoordinate(new Vertex.UvCoordinate(1.0f, 0.0f)) + .build()); + vertices.add(Vertex.builder() + .setPosition(new Vector3(-0.5f, 1.0f, 0.0f)) + .setNormal(new Vector3(0.0f, 0.0f, 1.0f)) + .setUvCoordinate(new Vertex.UvCoordinate(0.0f, 1.0f)) + .build()); + vertices.add(Vertex.builder() + .setPosition(new Vector3(0.5f, 1.0f, 0.0f)) + .setNormal(new Vector3(0.0f, 0.0f, 1.0f)) + .setUvCoordinate(new Vertex.UvCoordinate(1.0f, 1.0f)) + .build()); + ArrayList triangleIndices = new ArrayList<>(); + triangleIndices.add(0); + triangleIndices.add(1); + triangleIndices.add(2); + triangleIndices.add(1); + triangleIndices.add(3); + triangleIndices.add(2); + RenderableDefinition.Submesh submesh = + RenderableDefinition.Submesh.builder().setTriangleIndices(triangleIndices).setMaterial(material).build(); + setSource( + RenderableDefinition.builder() + .setVertices(vertices) + .setSubmeshes(Arrays.asList(submesh)) + .build() + ); + } + ); + return setSourceFuture.thenCompose((Void) -> super.build()); + } + + return super.build(); + } + + @Override + protected ViewRenderable makeRenderable() { + if (this.view != null) { + return new ViewRenderable(this, view); + } else { + return new ViewRenderable(this, inflateViewFromResourceId()); + } + } + + /** @hide */ + @Override + protected Class getRenderableClass() { + return ViewRenderable.class; + } + + /** @hide */ + @Override + protected ResourceRegistry getRenderableRegistry() { + return ResourceManager.getInstance().getViewRenderableRegistry(); + } + + /** @hide */ + @Override + protected Builder getSelf() { + return this; + } + + /** @hide */ + @SuppressWarnings("AndroidApiChecker") + @Override + protected void checkPreconditions() { + super.checkPreconditions(); + + boolean hasView = resourceId.isPresent() || view != null; + + if (!hasView) { + throw new AssertionError("ViewRenderable must have a source."); + } + + if (resourceId.isPresent() && view != null) { + throw new AssertionError( + "ViewRenderable must have a resourceId or a view as a source. This one has both."); + } + } + + @SuppressWarnings("AndroidApiChecker") + private View inflateViewFromResourceId() { + if (context == null) { + throw new AssertionError("Context cannot be null"); + } + + // Inflate the view in a detached state. + // We need a dummy ViewGroup as the root so that the layout params of the view are loaded. + ViewGroup dummy = new FrameLayout(context); + return LayoutInflater.from(context).inflate(resourceId.getAsInt(), dummy, false); + } + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewRenderableHelpers.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewRenderableHelpers.java new file mode 100644 index 0000000..fa7f332 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewRenderableHelpers.java @@ -0,0 +1,31 @@ +package com.google.ar.sceneform.rendering; + +import android.content.res.Resources; +import android.util.DisplayMetrics; +import android.view.View; + + +/** Helper class for utility functions for a view rendered in world space. */ + +class ViewRenderableHelpers { + /** Returns the aspect ratio of a view (width / height). */ + static float getAspectRatio(View view) { + float viewWidth = (float) view.getWidth(); + float viewHeight = (float) view.getHeight(); + + if (viewWidth == 0.0f || viewHeight == 0.0f) { + return 0.0f; + } + + return viewWidth / viewHeight; + } + + /** + * Returns the number of density independent pixels that a given number of pixels is equal to on + * this device. + */ + static float convertPxToDp(int px) { + DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics(); + return px / (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT); + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewRenderableInternalData.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewRenderableInternalData.java new file mode 100644 index 0000000..89e6d46 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewRenderableInternalData.java @@ -0,0 +1,29 @@ +package com.google.ar.sceneform.rendering; + + +import com.google.ar.sceneform.resources.SharedReference; +import com.google.ar.sceneform.utilities.AndroidPreconditions; + +/** + * Represents shared data used by {@link ViewRenderable}s for rendering. The data will be released + * when all {@link ViewRenderable}s using this data are finalized. + */ + +class ViewRenderableInternalData extends SharedReference { + private final RenderViewToExternalTexture renderView; + + ViewRenderableInternalData(RenderViewToExternalTexture renderView) { + this.renderView = renderView; + } + + RenderViewToExternalTexture getRenderView() { + return renderView; + } + + @Override + protected void onDispose() { + AndroidPreconditions.checkUiThread(); + + renderView.releaseResources(); + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewSizer.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewSizer.java new file mode 100644 index 0000000..af2d896 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/rendering/ViewSizer.java @@ -0,0 +1,24 @@ +package com.google.ar.sceneform.rendering; + +import android.view.View; + +import com.google.ar.sceneform.math.Vector3; + +/** + * Interface for controlling the size of a {@link ViewRenderable} in the {@link + * com.google.ar.sceneform.Scene}. The final size that the view is displayed at will be the size + * from this {@link ViewSizer} scaled by the {@link com.google.ar.sceneform.Node#getWorldScale()} of + * the {@link com.google.ar.sceneform.Node} that the {@link ViewRenderable} is attached to. + */ + +public interface ViewSizer { + /** + * Calculates the desired size of the view in the {@link com.google.ar.sceneform.Scene}. {@link + * Vector3#x} represents the width, and {@link Vector3#y} represents the height. + * + * @param view the view to calculate the size of + * @return a new vector that represents the view's size in the {@link + * com.google.ar.sceneform.Scene} + */ + Vector3 getSize(View view); +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/resources/ResourceHolder.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/resources/ResourceHolder.java new file mode 100644 index 0000000..afeec14 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/resources/ResourceHolder.java @@ -0,0 +1,14 @@ +package com.google.ar.sceneform.resources; + +/** Pool or cachce for resources */ +public interface ResourceHolder { + /** + * Polls for garbage collected objects and disposes associated data. + * + * @return Count of resources in use. + */ + long reclaimReleasedResources(); + + /** Ignores reference count and disposes any associated resources. */ + void destroyAllResources(); +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/resources/ResourceRegistry.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/resources/ResourceRegistry.java new file mode 100644 index 0000000..8e96a2c --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/resources/ResourceRegistry.java @@ -0,0 +1,147 @@ +package com.google.ar.sceneform.resources; + +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import com.google.ar.sceneform.utilities.Preconditions; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * ResourceRegistry keeps track of resources that have been loaded and are in the process of being + * loaded. The registry maintains only weak references and doesn't prevent resources from being + * collected. + * + * @hide + */ +// TODO: Automatically prune dead WeakReferences from ResourceRegistry when the +// ResourceRegistry becomes large. +public class ResourceRegistry implements ResourceHolder { + private static final String TAG = ResourceRegistry.class.getSimpleName(); + + private final Object lock = new Object(); + + @GuardedBy("lock") + private final Map> registry = new HashMap<>(); + + @GuardedBy("lock") + private final Map> futureRegistry = new HashMap<>(); + + /** + * Returns a future to a resource previously registered with the same id. If resource has not yet + * been registered or was garbage collected, returns null. The future may be to a resource that + * has already finished loading, in which case {@link CompletableFuture#isDone()} will be true. + */ + @Nullable + public CompletableFuture get(Object id) { + Preconditions.checkNotNull(id, "Parameter 'id' was null."); + + synchronized (lock) { + // If the resource has already finished loading, return a completed future to that resource. + WeakReference reference = registry.get(id); + if (reference != null) { + T resource = reference.get(); + if (resource != null) { + return CompletableFuture.completedFuture(resource); + } else { + registry.remove(id); + } + } + + // If the resource is in the process of loading, return the future directly. + // If the id is not registered, this will be null. + return futureRegistry.get(id); + } + } + + /** + * Registers a future to a resource by an id. If registering a resource that has already finished + * loading, use {@link CompletableFuture#completedFuture(Object)}. + */ + public void register(Object id, CompletableFuture futureResource) { + Preconditions.checkNotNull(id, "Parameter 'id' was null."); + Preconditions.checkNotNull(futureResource, "Parameter 'futureResource' was null."); + + // If the future is already completed, add it to the registry for resources that are loaded and + // return early. + if (futureResource.isDone()) { + if (futureResource.isCompletedExceptionally()) { + return; + } + + // Suppress warning for passing null into getNow. getNow isn't annotated, but it allows null. + // Also, there is a precondition check here anyways. + @SuppressWarnings("nullness") + T resource = Preconditions.checkNotNull(futureResource.getNow(null)); + + synchronized (lock) { + registry.put(id, new WeakReference<>(resource)); + + // If the id was previously registered in the futureRegistry, make sure it is removed. + futureRegistry.remove(id); + } + + return; + } + + synchronized (lock) { + futureRegistry.put(id, futureResource); + + // If the id was previously registered in the completed registry, make sure it is removed. + registry.remove(id); + } + + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + CompletableFuture registerFuture = + futureResource.handle( + (result, throwable) -> { + synchronized (this) { + // Check to make sure that the future in the registry is this future. + // Otherwise, this id has already been overwritten with another resource. + synchronized (lock) { + CompletableFuture futureReference = futureRegistry.get(id); + if (futureReference == futureResource) { + futureRegistry.remove(id); + if (throwable == null) { + // Only add a reference if there was no exception. + registry.put(id, new WeakReference<>(result)); + } + } + } + } + return null; + }); + } + + /** + * Removes all cache entries. Cancels any in progress futures. cancel does not interrupt work in + * progress. It only prevents the final stage from starting. + */ + @Override + public void destroyAllResources() { + synchronized (lock) { + Iterator>> iterator = + futureRegistry.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + iterator.remove(); + CompletableFuture futureResource = entry.getValue(); + if (!futureResource.isDone()) { + futureResource.cancel(true); + } + } + + registry.clear(); + } + } + + @Override + public long reclaimReleasedResources() { + // Resources held in registry are also held by other ResourceHolders. Return zero for this one + // and do + // counting in the other holders. + return 0; + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/resources/SharedReference.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/resources/SharedReference.java new file mode 100644 index 0000000..562302c --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/resources/SharedReference.java @@ -0,0 +1,29 @@ +package com.google.ar.sceneform.resources; + +/** + * Used for managing memory of shared object using reference counting. + * + * @hide + */ +public abstract class SharedReference { + private int referenceCount = 0; + + public void retain() { + referenceCount++; + } + + public void release() { + referenceCount--; + dispose(); + } + + protected abstract void onDispose(); + + private void dispose() { + if (referenceCount > 0) { + return; + } + + onDispose(); + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/AndroidPreconditions.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/AndroidPreconditions.java new file mode 100644 index 0000000..2b94dba --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/AndroidPreconditions.java @@ -0,0 +1,79 @@ +package com.google.ar.sceneform.utilities; + +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.Looper; +import androidx.annotation.VisibleForTesting; + +/** + * Helper class for common android specific preconditions used inside of RenderCore. + * + * @hide + */ +public class AndroidPreconditions { + private static final boolean IS_ANDROID_API_AVAILABLE = checkAndroidApiAvailable(); + private static final boolean IS_MIN_ANDROID_API_LEVEL = isMinAndroidApiLevelImpl(); + private static boolean isUnderTesting = false; + + /** + * Ensure that the code is being executed on Android's UI thread. Null-Op if the Android API isn't + * available (i.e. for unit tests. + */ + public static void checkUiThread() { + if (!isAndroidApiAvailable() || isUnderTesting()) { + return; + } + + boolean isOnUIThread = Looper.getMainLooper().getThread() == Thread.currentThread(); + Preconditions.checkState(isOnUIThread, "Must be called from the UI thread."); + } + + /** + * Enforce the minimum Android api level + * + * @throws IllegalStateException if the api level is not high enough + */ + public static void checkMinAndroidApiLevel() { + Preconditions.checkState(isMinAndroidApiLevel(), "Sceneform requires Android N or later"); + } + + /** + * Returns true if the Android API is currently available. Useful for branching functionality to + * make it testable via junit. The android API is available for Robolectric tests and android + * emulator tests. + */ + public static boolean isAndroidApiAvailable() { + return IS_ANDROID_API_AVAILABLE; + } + + public static boolean isUnderTesting() { + return isUnderTesting; + } + + /** + * Returns true if the Android api level is above the minimum or if not on Android. + * + *

Also returns true if not on Android or in a test. + */ + public static boolean isMinAndroidApiLevel() { + return isUnderTesting() || IS_MIN_ANDROID_API_LEVEL; + } + + @VisibleForTesting + public static void setUnderTesting(boolean isUnderTesting) { + AndroidPreconditions.isUnderTesting = isUnderTesting; + } + + private static boolean isMinAndroidApiLevelImpl() { + return !isAndroidApiAvailable() || (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP); + } + + private static boolean checkAndroidApiAvailable() { + try { + Class.forName("android.app.Activity"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/ArCoreVersion.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/ArCoreVersion.java new file mode 100644 index 0000000..9f2e71a --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/ArCoreVersion.java @@ -0,0 +1,36 @@ +package com.google.ar.sceneform.utilities; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Bundle; + +/** + * Utilities for detecting and handling the version of Ar Core. + * + * @hide + */ +public class ArCoreVersion { + public static final int VERSION_CODE_1_3 = 180604036; + + private static final String METADATA_KEY_MIN_APK_VERSION = "com.google.ar.core.min_apk_version"; + + public static int getMinArCoreVersionCode(Context context) { + PackageManager packageManager = context.getPackageManager(); + String packageName = context.getPackageName(); + + Bundle metadata; + try { + metadata = + packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).metaData; + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException("Could not load application package metadata.", e); + } + + if (metadata.containsKey(METADATA_KEY_MIN_APK_VERSION)) { + return metadata.getInt(METADATA_KEY_MIN_APK_VERSION); + } else { + throw new IllegalStateException( + "Application manifest must contain meta-data." + METADATA_KEY_MIN_APK_VERSION); + } + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/ChangeId.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/ChangeId.java new file mode 100644 index 0000000..c103bec --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/ChangeId.java @@ -0,0 +1,38 @@ +package com.google.ar.sceneform.utilities; + +/** + * Used to identify when the state of an object has changed by incrementing an integer id. Other + * classes can determine when this object has changed by polling to see if the id has changed. + * + *

This is useful as an alternative to an event listener subscription model when there is no safe + * point in the lifecycle of an object to unsubscribe from the event listeners. Unlike event + * listeners, this cannot cause memory leaks. + * + * @hide + */ +public class ChangeId { + public static final int EMPTY_ID = 0; + + private int id = EMPTY_ID; + + public int get() { + return id; + } + + public boolean isEmpty() { + return id == EMPTY_ID; + } + + public boolean checkChanged(int id) { + return this.id != id && !isEmpty(); + } + + public void update() { + id++; + + // Skip EMPTY_ID if the id has cycled all the way around. + if (id == EMPTY_ID) { + id++; + } + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/EnvironmentalHdrParameters.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/EnvironmentalHdrParameters.java new file mode 100644 index 0000000..4317c53 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/EnvironmentalHdrParameters.java @@ -0,0 +1,93 @@ +package com.google.ar.sceneform.utilities; + +/** + * Provides scaling factors from Environmental Hdr to Filament. + * + *

A conversion is required to convert between Environmental Hdr units and an intensity value + * Filament can use. + * + * @hide This class may be removed eventually. + */ +// TODO: Replace each of these values with principled numbers. +public class EnvironmentalHdrParameters { + public static final float DEFAULT_AMBIENT_SH_SCALE_FOR_FILAMENT = 1.0f; + public static final float DEFAULT_DIRECT_INTENSITY_FOR_FILAMENT = 1.0f; + public static final float DEFAULT_REFLECTION_SCALE_FOR_FILAMENT = 1.0f; + + /** Builds ViewerConfig, a collection of runtime config options for the viewer. */ + public static class Builder { + public Builder() {} + + public EnvironmentalHdrParameters build() { + return new EnvironmentalHdrParameters(this); + } + + /** Conversion factor for directional lighting. */ + public Builder setDirectIntensityForFilament(float directIntensityForFilament) { + this.directIntensityForFilament = directIntensityForFilament; + return this; + } + + /** Conversion factor for ambient spherical harmonics. */ + public Builder setAmbientShScaleForFilament(float ambientShScaleForFilament) { + this.ambientShScaleForFilament = ambientShScaleForFilament; + return this; + } + + /** Conversion factor for reflections. */ + public Builder setReflectionScaleForFilament(float reflectionScaleForFilament) { + this.reflectionScaleForFilament = reflectionScaleForFilament; + return this; + } + + private float ambientShScaleForFilament; + private float directIntensityForFilament; + private float reflectionScaleForFilament; + } + + /** Constructs a builder, all required fields must be specified. */ + public static EnvironmentalHdrParameters.Builder builder() { + return new EnvironmentalHdrParameters.Builder(); + } + + public static EnvironmentalHdrParameters makeDefault() { + return builder() + .setAmbientShScaleForFilament(DEFAULT_AMBIENT_SH_SCALE_FOR_FILAMENT) + .setDirectIntensityForFilament(DEFAULT_DIRECT_INTENSITY_FOR_FILAMENT) + .setReflectionScaleForFilament(DEFAULT_REFLECTION_SCALE_FOR_FILAMENT) + .build(); + } + + private EnvironmentalHdrParameters(Builder builder) { + ambientShScaleForFilament = builder.ambientShScaleForFilament; + directIntensityForFilament = builder.directIntensityForFilament; + reflectionScaleForFilament = builder.reflectionScaleForFilament; + } + + /** + * A scale factor bridging Environmental Hdr's ambient sh to Filament's ambient sh values. + * + *

This number has been hand tuned by comparing lighting to reference app + * /third_party/arcore/unity/apps/whitebox + */ + public float getAmbientShScaleForFilament() { + return ambientShScaleForFilament; + } + + /** Environmental Hdr provides a relative intensity, a number above zero and often below 8. */ + public float getDirectIntensityForFilament() { + return directIntensityForFilament; + } + + /** + * A scale factor bridging Environmental Hdr's relative intensity to a lux based intensity for + * reflections only. + */ + public float getReflectionScaleForFilament() { + return reflectionScaleForFilament; + } + + private final float ambientShScaleForFilament; + private final float directIntensityForFilament; + private final float reflectionScaleForFilament; +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/LoadHelper.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/LoadHelper.java new file mode 100644 index 0000000..56956e5 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/LoadHelper.java @@ -0,0 +1,371 @@ +package com.google.ar.sceneform.utilities; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.net.Uri; +import android.net.http.HttpResponseCache; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; +import androidx.annotation.Nullable; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Callable; + +/** + * Convenience class to parse Uri's. + * + * @hide + */ +public class LoadHelper { + private static final String TAG = LoadHelper.class.getName(); + // From https://developer.android.com/reference/android/content/res/Resources + // The value 0 is an invalid identifier. + public static final int INVALID_RESOURCE_IDENTIFIER = 0; + private static final String RAW_RESOURCE_TYPE = "raw"; + private static final String DRAWABLE_RESOURCE_TYPE = "drawable"; + private static final char SLASH_DELIMETER = '/'; + private static final String ANDROID_ASSET = SLASH_DELIMETER + "android_asset" + SLASH_DELIMETER; + // Default cache size of 512MB. + private static final long DEFAULT_CACHE_SIZE_BYTES = 512 << 20; + + /** Static utility class */ + private LoadHelper() {} + + /** True if the Uri is an Android resource, false if any other uri. */ + public static Boolean isAndroidResource(Uri sourceUri) { + Preconditions.checkNotNull(sourceUri, "Parameter \"sourceUri\" was null."); + return TextUtils.equals(ContentResolver.SCHEME_ANDROID_RESOURCE, sourceUri.getScheme()); + } + + /** True if the Uri is a filename, false if it is a remote location. */ + public static Boolean isFileAsset(Uri sourceUri) { + Preconditions.checkNotNull(sourceUri, "Parameter \"sourceUri\" was null."); + @Nullable String scheme = sourceUri.getScheme(); + return TextUtils.isEmpty(scheme) || Objects.equals(ContentResolver.SCHEME_FILE, scheme); + } + + /** + * Normalizes Uri's based on a reference Uri. This function is for convenience only since the Uri + * class can do this as well. + */ + public static Uri resolveUri(Uri unresolvedUri, @Nullable Uri parentUri) { + + if (parentUri == null) { + return unresolvedUri; + } else { + return resolve(parentUri, unresolvedUri); + } + } + + /** + * Creates an InputStream from an Android resource ID. + * + * @throws IllegalArgumentException for resources that can't be loaded. + */ + public static Callable fromResource(Context context, int resId) { + Preconditions.checkNotNull(context, "Parameter \"context\" was null."); + + String resourceType = context.getResources().getResourceTypeName(resId); + if (resourceType.equals(RAW_RESOURCE_TYPE) || resourceType.equals(DRAWABLE_RESOURCE_TYPE)) { + return () -> context.getResources().openRawResource(resId); + } else { + throw new IllegalArgumentException( + "Unknown resource resourceType '" + + resourceType + + "' in resId '" + + context.getResources().getResourceName(resId) + + "'. Resource will not be loaded"); + } + } + + /** + * Creates different InputStreams depending on the contents of the Uri + * + * @throws IllegalArgumentException for Uri's that can't be loaded. + */ + public static Callable fromUri(Context context, Uri sourceUri) { + return fromUri(context, sourceUri, null); + } + + /** + * Creates different InputStreams depending on the contents of the Uri. + * + * @param requestProperty Adds connection properties to created input stream. + * @throws IllegalArgumentException for Uri's that can't be loaded. + */ + public static Callable fromUri( + Context context, Uri sourceUri, @Nullable Map requestProperty) { + Preconditions.checkNotNull(sourceUri, "Parameter \"sourceUri\" was null."); + Preconditions.checkNotNull(context, "Parameter \"context\" was null."); + if (isFileAsset(sourceUri)) { + return fileUriToInputStreamCreator(context, sourceUri); + } else if (isAndroidResource(sourceUri)) { + // Note: Prefer creating InputStreams directly from resources. + // By converting to URIs first, we can't load library resources from a dynamic module. + return androidResourceUriToInputStreamCreator(context, sourceUri); + } else if (isGltfDataUri(sourceUri)) { + return dataUriInputStreamCreator(sourceUri); + } + return remoteUriToInputStreamCreator(sourceUri, requestProperty); + } + + /** + * Generates a Uri from an Android resource. + * + * @throws Resources.NotFoundException + */ + public static Uri resourceToUri(Context context, int resID) { + Resources resources = context.getResources(); + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(resID)) + .appendPath(resources.getResourceTypeName(resID)) + .appendPath(resources.getResourceEntryName(resID)) + .build(); + } + + /** Return the integer resource id for the specified resource name. */ + public static int rawResourceNameToIdentifier(Context context, String name) { + return context.getResources().getIdentifier(name, RAW_RESOURCE_TYPE, context.getPackageName()); + } + + /** Return the integer resource id for the specified resource name. */ + public static int drawableResourceNameToIdentifier(Context context, String name) { + return context + .getResources() + .getIdentifier(name, DRAWABLE_RESOURCE_TYPE, context.getPackageName()); + } + + /** + * Enables HTTP caching with default settings, remote Uri requests responses are cached to + * cacheBaseDir/cacheFolderName + */ + public static void enableCaching(Context context) { + enableCaching(DEFAULT_CACHE_SIZE_BYTES, context.getCacheDir(), "http_cache"); + } + + /** + * Enables HTTP caching, remote Uri requests responses are cached to cacheBaseDir/cacheFolderName + */ + public static void enableCaching(long cacheByteSize, File cacheBaseDir, String cacheFolderName) { + // Define the default response cache if it has been previously defined. + if (HttpResponseCache.getInstalled() == null) { + try { + File httpCacheDir = new File(cacheBaseDir, cacheFolderName); + if (VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH) { + HttpResponseCache.install(httpCacheDir, cacheByteSize); + } + } catch (IOException e) { + Log.i(TAG, "HTTP response cache installation failed:" + e); + } + } + } + + public static void flushHttpCache() { + HttpResponseCache cache = HttpResponseCache.getInstalled(); + if (cache != null) { + cache.flush(); + } + } + + /** Creates an inputStream to read from asset file */ + // TODO: Fix nullness violation: dereference of possibly-null reference + // sourceUri.getPath() + @SuppressWarnings("nullness:dereference.of.nullable") + private static Callable fileUriToInputStreamCreator(Context context, Uri sourceUri) { + AssetManager assetManager = context.getAssets(); + String filename; + if (sourceUri.getAuthority() == null) { + filename = sourceUri.getPath(); + } else if (sourceUri.getPath().isEmpty()) { + filename = sourceUri.getAuthority(); + } else { + filename = sourceUri.getAuthority() + sourceUri.getPath(); + } + + // Remove "android_asset/" from URI paths like "file:///android_asset/...". + // TODO: Fix nullness violation: incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") + String scrubbedFilename = removeAndroidAssetPath(filename); + + return () -> { + if (assetExists(assetManager, scrubbedFilename)) { + // Open Android Asset if an Asset was found + return assetManager.open(scrubbedFilename); + } else { + // Open file from storage or other non asset location. + return new FileInputStream(new File(filename)); + } + }; + } + + private static String removeAndroidAssetPath(String filename) { + // Remove "android_asset/" from URI paths like "file:///android_asset/...". + String scrubbedFilename = filename; + if (filename.startsWith(ANDROID_ASSET)) { + scrubbedFilename = filename.substring(ANDROID_ASSET.length()); + } + return scrubbedFilename; + } + + /** + * Creates an inputStream to read from android resource + * + * @throws IllegalArgumentException for resources that can't be loaded. + */ + // TODO: incompatible types in return. + @SuppressWarnings("nullness:return.type.incompatible") + private static Callable androidResourceUriToInputStreamCreator( + Context context, Uri sourceUri) { + String sourceUriPath = sourceUri.getPath(); + // TODO: Fix nullness violation: dereference of possibly-null reference + // sourceUriPath + @SuppressWarnings("nullness:dereference.of.nullable") + int lastSlashIndex = sourceUriPath.lastIndexOf(SLASH_DELIMETER); + String resourceType = sourceUriPath.substring(1, lastSlashIndex); + + if (resourceType.equals(RAW_RESOURCE_TYPE) || resourceType.equals(DRAWABLE_RESOURCE_TYPE)) { + return () -> context.getContentResolver().openInputStream(sourceUri); + } else { + throw new IllegalArgumentException( + "Unknown resource resourceType '" + + resourceType + + "' in uri '" + + sourceUri + + "'. Resource will not be loaded"); + } + } + + /** + * Creates an inputStream to read from remote URL + * + * @throws IllegalArgumentException for URL's that can't be loaded. + */ + private static Callable remoteUriToInputStreamCreator( + Uri sourceUri, @Nullable Map requestProperty) { + try { + URL sourceURL = new URL(sourceUri.toString()); + URLConnection conn = sourceURL.openConnection(); + // Apply properties to the connection if they are available. + if (requestProperty != null) { + for (Map.Entry entry : requestProperty.entrySet()) { + conn.addRequestProperty(entry.getKey(), entry.getValue()); + } + } + return () -> conn.getInputStream(); + } catch (MalformedURLException ex) { + // This is rare. Most bad URL's get filtered out when the URL class is constructed. + throw new IllegalArgumentException("Unable to parse url: \'" + sourceUri + "'", ex); + } catch (IOException e) { + throw new AssertionError("Error opening url connection: '" + sourceUri + "'", e); + } + } + + private static Uri resolve(Uri parent, Uri child) { + try { + URI javaParentUri = new URI(parent.toString()); + URI javaChildUri = new URI(child.toString()); + URI resolvedUri = javaParentUri.resolve(javaChildUri); + return Uri.parse(resolvedUri.toString()); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException("Unable to parse Uri.", ex); + } + } + + private static boolean assetExists(AssetManager assetManager, String assetRelativePath) + throws IOException { + String targetAssetName; + String[] assetsInSameDirectory; + int lastSlashIndex = assetRelativePath.lastIndexOf(SLASH_DELIMETER); + + if (lastSlashIndex != -1) { + targetAssetName = assetRelativePath.substring(lastSlashIndex + 1); + assetsInSameDirectory = assetManager.list(assetRelativePath.substring(0, lastSlashIndex)); + } else { + targetAssetName = assetRelativePath; + assetsInSameDirectory = assetManager.list(""); + } + + if (assetsInSameDirectory != null) { + // Search for Android Asset in given directory. + for (String assetName : assetsInSameDirectory) { + if (targetAssetName.equals(assetName)) { + return true; + } + } + } + return false; + } + + public static boolean isDataUri(Uri uri) { + String scheme = uri.getScheme(); + return scheme != null && scheme.equals("data"); + } + + public static boolean isGltfDataUri(Uri uri) { + if (!isDataUri(uri)) { + return false; + } else { + return getGltfExtensionFromSchemeSpecificPart(uri.getSchemeSpecificPart()) != null; + } + } + + @Nullable + private static String getGltfExtensionFromSchemeSpecificPart(String schemeSpecificPart) { + if (schemeSpecificPart.startsWith("model/gltf-binary")) { + return "glb"; + } + if (schemeSpecificPart.startsWith("model/gltf+json")) { + return "gltf"; + } + return null; + } + + /** + * Creates an inputStream to read from a data URI. + * + * @throws IllegalArgumentException for URL's that can't be loaded. + */ + private static Callable dataUriInputStreamCreator(Uri uri) { + String data = uri.getSchemeSpecificPart(); + int commaIndex = data.indexOf(','); + if (commaIndex < 0) { + throw new IllegalArgumentException("Malformed data uri - does not contain a ','"); + } + String prefix = data.substring(0, commaIndex); + boolean isBase64 = prefix.contains(";base64"); + String dataString = data.substring(commaIndex + 1); + return () -> + new ByteArrayInputStream( + isBase64 ? Base64.decode(dataString, Base64.DEFAULT) : dataString.getBytes()); + } + + public static String getLastPathSegment(Uri uri) { + if (isGltfDataUri(uri)) { + return "file." + getGltfExtensionFromSchemeSpecificPart(uri.getSchemeSpecificPart()); + } else { + String lastPathSegment = uri.getLastPathSegment(); + if (lastPathSegment == null) { + // This could be a file:// uri, e.g. if it's loaded out of assets. + String uriString = uri.toString(); + lastPathSegment = uriString.substring(uriString.lastIndexOf('/') + 1); + } + return lastPathSegment; + } + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/MovingAverage.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/MovingAverage.java new file mode 100644 index 0000000..15e20e3 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/MovingAverage.java @@ -0,0 +1,53 @@ +package com.google.ar.sceneform.utilities; + +/** + * Calculates an exponentially weighted moving average for a series of data. + * + * @hide + */ +public class MovingAverage { + private double average; + private final double weight; + + public static final double DEFAULT_WEIGHT = 0.9f; + + /** + * Construct an object to track the exponentially weighted moving average for a series of data. + * The weight is set to a default of 0.9, which is good for data with lots of samples when the + * average should be resistant to spikes (i.e. frame rate). + * + *

The weight is a ratio between 0 and 1 that represents how much of the previous average is + * kept compared to the new sample. With a weight of 0.9, 90% of the previous average is kept and + * 10% of the new sample is added to the average. + * + * @param initialSample the first sample in the average + */ + public MovingAverage(double initialSample) { + this(initialSample, DEFAULT_WEIGHT); + } + + /** + * Construct an object to track the exponentially weighted moving average for a series of data. + * + *

The weight is a ratio between 0 and 1 that represents how much of the previous average is + * kept compared to the new sample. With a weight of 0.9, 90% of the previous average is kept and + * 10% of the new sample is added to the average. + * + * @param initialSample the first sample in the average + * @param weight the weight to used when adding samples + */ + public MovingAverage(double initialSample, double weight) { + average = initialSample; + this.weight = weight; + } + + /** Add a sample and calculate a new average. */ + public void addSample(double sample) { + average = weight * average + (1.0 - weight) * sample; + } + + /** Returns the current average for all samples. */ + public double getAverage() { + return average; + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/MovingAverageMillisecondsTracker.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/MovingAverageMillisecondsTracker.java new file mode 100644 index 0000000..6af7830 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/MovingAverageMillisecondsTracker.java @@ -0,0 +1,80 @@ +package com.google.ar.sceneform.utilities; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +/** + * Used to track a {@link MovingAverage} that represents the number of milliseconds that elapse + * within the execution of a block of code. + * + * @hide + */ +public class MovingAverageMillisecondsTracker { + private static final double NANOSECONDS_TO_MILLISECONDS = 0.000001; + + interface Clock { + long getNanoseconds(); + } + + private static class DefaultClock implements Clock { + @Override + public long getNanoseconds() { + return System.nanoTime(); + } + } + + @Nullable private MovingAverage movingAverage; + private final double weight; + private final Clock clock; + private long beginSampleTimestampNano; + + public MovingAverageMillisecondsTracker() { + this(MovingAverage.DEFAULT_WEIGHT); + } + + public MovingAverageMillisecondsTracker(double weight) { + this.weight = weight; + clock = new DefaultClock(); + } + + @VisibleForTesting + public MovingAverageMillisecondsTracker(Clock clock) { + this(clock, MovingAverage.DEFAULT_WEIGHT); + } + + @VisibleForTesting + public MovingAverageMillisecondsTracker(Clock clock, double weight) { + this.weight = weight; + this.clock = clock; + } + + /** + * Call at the point in execution when the tracker should start measuring elapsed milliseconds. + */ + public void beginSample() { + beginSampleTimestampNano = clock.getNanoseconds(); + } + + /** + * Call at the point in execution when the tracker should stop measuring elapsed milliseconds and + * post a new sample. + */ + public void endSample() { + long sampleNano = clock.getNanoseconds() - beginSampleTimestampNano; + double sample = sampleNano * NANOSECONDS_TO_MILLISECONDS; + + if (movingAverage == null) { + movingAverage = new MovingAverage(sample, weight); + } else { + movingAverage.addSample(sample); + } + } + + public double getAverage() { + if (movingAverage != null) { + return movingAverage.getAverage(); + } + + return 0.0; + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/Preconditions.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/Preconditions.java new file mode 100644 index 0000000..2e4d934 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/Preconditions.java @@ -0,0 +1,157 @@ +package com.google.ar.sceneform.utilities; + +import androidx.annotation.Nullable; + +/** + * Static convenience methods that help a method or constructor check whether it was invoked + * correctly. + * + * @hide + */ +// See /third_party/java_src/google_common/java7/java/com/google/common/base/Preconditions.java +// We have written our own version to avoid adding a dependency on a large library. +public class Preconditions { + /** + * Ensures that an object reference passed as a parameter to the calling method is not null. + * + * @param reference an object reference + * @throws NullPointerException if {@code reference} is null + */ + public static T checkNotNull(@Nullable T reference) { + if (reference == null) { + throw new NullPointerException(); + } + + return reference; + } + + /** + * Ensures that an object reference passed as a parameter to the calling method is not null. + * + * @param reference an object reference + * @param errorMessage the exception message to use if the check fails; will be converted to a + * string using {@link String#valueOf(Object)} + * @throws NullPointerException if {@code reference} is null + */ + public static T checkNotNull(@Nullable T reference, Object errorMessage) { + if (reference == null) { + throw new NullPointerException(String.valueOf(errorMessage)); + } + + return reference; + } + + /** + * Ensures that {@code index} specifies a valid element in an array, list or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, list or string + * @param size the size of that array, list or string + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static void checkElementIndex(int index, int size) { + checkElementIndex(index, size, "index"); + } + + /** + * Ensures that {@code index} specifies a valid element in an array, list or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, list or string + * @param size the size of that array, list or string + * @param desc the text to use to describe this index in an error message + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static void checkElementIndex(int index, int size, String desc) { + // Carefully optimized for execution by hotspot (explanatory comment above) + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(badElementIndex(index, size, desc)); + } + } + + /** + * Ensures the truth of an expression involving the state of the calling instance, but not + * involving any parameters to the calling method. + * + * @param expression a boolean expression + * @throws IllegalStateException if {@code expression} is false + */ + public static void checkState(boolean expression) { + if (!expression) { + throw new IllegalStateException(); + } + } + + /** + * Ensures the truth of an expression involving the state of the calling instance, but not + * involving any parameters to the calling method. + * + * @param expression a boolean expression + * @param errorMessage the exception message to use if the check fails; will be converted to a + * string using {@link String#valueOf(Object)} + * @throws IllegalStateException if {@code expression} is false + */ + public static void checkState(boolean expression, @Nullable Object errorMessage) { + if (!expression) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + } + + private static String badElementIndex(int index, int size, String desc) { + if (index < 0) { + return format("%s (%s) must not be negative", desc, index); + } else if (size < 0) { + throw new IllegalArgumentException("negative size: " + size); + } else { // index >= size + return format("%s (%s) must be less than size (%s)", desc, index, size); + } + } + + /** + * Substitutes each {@code %s} in {@code template} with an argument. These are matched by + * position: the first {@code %s} gets {@code args[0]}, etc. If there are more arguments than + * placeholders, the unmatched arguments will be appended to the end of the formatted message in + * square braces. + * + * @param template a string containing 0 or more {@code %s} placeholders. null is treated as + * "null". + * @param args the arguments to be substituted into the message template. Arguments are converted + * to strings using {@link String#valueOf(Object)}. Arguments can be null. + */ + // Note that this is somewhat-improperly used from Verify.java as well. + private static String format(String template, Object... args) { + template = String.valueOf(template); // null -> "null" + + args = args == null ? new Object[] {"(Object[])null"} : args; + + // start substituting the arguments into the '%s' placeholders + StringBuilder builder = new StringBuilder(template.length() + 16 * args.length); + int templateStart = 0; + int i = 0; + while (i < args.length) { + int placeholderStart = template.indexOf("%s", templateStart); + if (placeholderStart == -1) { + break; + } + builder.append(template, templateStart, placeholderStart); + builder.append(args[i++]); + templateStart = placeholderStart + 2; + } + builder.append(template, templateStart, template.length()); + + // if we run out of placeholders, append the extra args in square braces + if (i < args.length) { + builder.append(" ["); + builder.append(args[i++]); + while (i < args.length) { + builder.append(", "); + builder.append(args[i++]); + } + builder.append(']'); + } + + return builder.toString(); + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/SceneformBufferUtils.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/SceneformBufferUtils.java new file mode 100644 index 0000000..a08ab7a --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/SceneformBufferUtils.java @@ -0,0 +1,130 @@ +package com.google.ar.sceneform.utilities; + +import android.content.res.AssetManager; +import android.util.Log; +import androidx.annotation.Nullable; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; + +/** + * A simple class to read InputStreams Once the data is read it can be accessed as a ByteBuffer. + * + * @hide + */ +public final class SceneformBufferUtils { + private static final String TAG = SceneformBufferUtils.class.getSimpleName(); + private static final int DEFAULT_BLOCK_SIZE = 8192; + + private SceneformBufferUtils() {} + + @Nullable + public static ByteBuffer readFile(AssetManager assets, String path) { + // TODO: this method/class may be replaceable by SourceBytes + InputStream inputStream = null; + try { + inputStream = assets.open(path); + } catch (IOException ex) { + Log.e(TAG, "Failed to read file " + path + " - " + ex.getMessage()); + return null; + } + + ByteBuffer buffer = readStream(inputStream); + + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ex) { + Log.e(TAG, "Failed to close the input stream from file " + path + " - " + ex.getMessage()); + } + } + + return buffer; + } + + @Nullable + public static ByteBuffer readStream(@Nullable InputStream inputStream) { + // TODO: this method/class may be replaceable by SourceBytes + ByteBuffer buffer = null; + if (inputStream == null) { + return buffer; + } + + try { + // Try to read the data from the inputStream + byte[] bytes = inputStreamToByteArray(inputStream); + buffer = ByteBuffer.wrap(bytes); + } catch (IOException ex) { + Log.e(TAG, "Failed to read stream - " + ex.getMessage()); + } + + return buffer; + } + + private static int copy(InputStream in, OutputStream out) throws IOException { + // TODO: this method/class may be replaceable by SourceBytes + byte[] buffer = new byte[DEFAULT_BLOCK_SIZE]; + int size = 0; + int n; + while ((n = in.read(buffer)) > 0) { + size += n; + out.write(buffer, 0, n); + } + out.flush(); + return size; + } + + public static byte[] copyByteBufferToArray(ByteBuffer in) throws IOException { + // TODO: this method/class may be replaceable by SourceBytes + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[DEFAULT_BLOCK_SIZE]; + int end = in.limit(); + int bytesRead; + while (in.position() < end) { + int lastPosition = in.position(); + + in.get(buffer, 0, Math.min(DEFAULT_BLOCK_SIZE, end - lastPosition)); + bytesRead = in.position() - lastPosition; + + out.write(buffer, 0, bytesRead); + } + out.flush(); + return out.toByteArray(); + } + + public static ByteBuffer copyByteBuffer(ByteBuffer in) throws IOException { + return ByteBuffer.wrap(copyByteBufferToArray(in)); + } + + public static ByteBuffer inputStreamToByteBuffer(Callable inputStreamCreator) { + ByteBuffer result; + try (InputStream inputStream = inputStreamCreator.call()) { + result = SceneformBufferUtils.readStream(inputStream); + } catch (Exception e) { + throw new CompletionException(e); + } + if (result == null) { + throw new AssertionError("Failed reading data from stream"); + } + return result; + } + + public static byte[] inputStreamCallableToByteArray(Callable inputStreamCreator) + throws Exception { + try (InputStream input = inputStreamCreator.call()) { + return inputStreamToByteArray(input); + } finally { + // Propagate exceptions up. + } + } + + public static byte[] inputStreamToByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/TimeAccumulator.java b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/TimeAccumulator.java new file mode 100644 index 0000000..2ca125d --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/ar/sceneform/utilities/TimeAccumulator.java @@ -0,0 +1,21 @@ +package com.google.ar.sceneform.utilities; + +/** Sums time samples together. Used for tracking the time elapsed of a set of code blocks. */ +public class TimeAccumulator { + private long elapsedTimeMs; + private long startSampleTimeMs; + + public void beginSample() { + startSampleTimeMs = System.currentTimeMillis(); + } + + public void endSample() { + long endSampleTimeMs = System.currentTimeMillis(); + long sampleMs = endSampleTimeMs - startSampleTimeMs; + elapsedTimeMs += sampleMs; + } + + public long getElapsedTimeMs() { + return elapsedTimeMs; + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Constants.java b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Constants.java new file mode 100644 index 0000000..751f4a6 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Constants.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * 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. + */ + +package com.google.flatbuffers; + +/// @cond FLATBUFFERS_INTERNAL + +/** + * Class that holds shared constants + */ +public class Constants { + // Java doesn't seem to have these. + /** The number of bytes in an `byte`. */ + static final int SIZEOF_BYTE = 1; + /** The number of bytes in a `short`. */ + static final int SIZEOF_SHORT = 2; + /** The number of bytes in an `int`. */ + static final int SIZEOF_INT = 4; + /** The number of bytes in an `float`. */ + static final int SIZEOF_FLOAT = 4; + /** The number of bytes in an `long`. */ + static final int SIZEOF_LONG = 8; + /** The number of bytes in an `double`. */ + static final int SIZEOF_DOUBLE = 8; + /** The number of bytes in a file identifier. */ + static final int FILE_IDENTIFIER_LENGTH = 4; + /** The number of bytes in a size prefix. */ + public static final int SIZE_PREFIX_LENGTH = 4; +} + +/// @endcond diff --git a/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/FlatBufferBuilder.java b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/FlatBufferBuilder.java new file mode 100644 index 0000000..80ab428 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/FlatBufferBuilder.java @@ -0,0 +1,1055 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * 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. + */ + +package com.google.flatbuffers; + +import static com.google.flatbuffers.Constants.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.*; +import java.util.Arrays; + +/// @file +/// @addtogroup flatbuffers_java_api +/// @{ + +/** + * Class that helps you build a FlatBuffer. See the section + * "Use in Java/C#" in the main FlatBuffers documentation. + */ +public class FlatBufferBuilder { + /// @cond FLATBUFFERS_INTERNAL + ByteBuffer bb; // Where we construct the FlatBuffer. + int space; // Remaining space in the ByteBuffer. + int minalign = 1; // Minimum alignment encountered so far. + int[] vtable = null; // The vtable for the current table. + int vtable_in_use = 0; // The amount of fields we're actually using. + boolean nested = false; // Whether we are currently serializing a table. + boolean finished = false; // Whether the buffer is finished. + int object_start; // Starting offset of the current struct/table. + int[] vtables = new int[16]; // List of offsets of all vtables. + int num_vtables = 0; // Number of entries in `vtables` in use. + int vector_num_elems = 0; // For the current vector being built. + boolean force_defaults = false; // False omits default values from the serialized data. + ByteBufferFactory bb_factory; // Factory for allocating the internal buffer + final Utf8 utf8; // UTF-8 encoder to use + /// @endcond + + /** + * Start with a buffer of size `initial_size`, then grow as required. + * + * @param initial_size The initial size of the internal buffer to use. + * @param bb_factory The factory to be used for allocating the internal buffer + */ + public FlatBufferBuilder(int initial_size, ByteBufferFactory bb_factory) { + this(initial_size, bb_factory, null, Utf8.getDefault()); + } + + /** + * Start with a buffer of size `initial_size`, then grow as required. + * + * @param initial_size The initial size of the internal buffer to use. + * @param bb_factory The factory to be used for allocating the internal buffer + * @param existing_bb The byte buffer to reuse. + * @param utf8 The Utf8 codec + */ + public FlatBufferBuilder(int initial_size, ByteBufferFactory bb_factory, + ByteBuffer existing_bb, Utf8 utf8) { + if (initial_size <= 0) { + initial_size = 1; + } + space = initial_size; + this.bb_factory = bb_factory; + if (existing_bb != null) { + bb = existing_bb; + bb.clear(); + bb.order(ByteOrder.LITTLE_ENDIAN); + } else { + bb = bb_factory.newByteBuffer(initial_size); + } + this.utf8 = utf8; + } + + /** + * Start with a buffer of size `initial_size`, then grow as required. + * + * @param initial_size The initial size of the internal buffer to use. + */ + public FlatBufferBuilder(int initial_size) { + this(initial_size, HeapByteBufferFactory.INSTANCE, null, Utf8.getDefault()); + } + + /** + * Start with a buffer of 1KiB, then grow as required. + */ + public FlatBufferBuilder() { + this(1024); + } + + /** + * Alternative constructor allowing reuse of {@link ByteBuffer}s. The builder + * can still grow the buffer as necessary. User classes should make sure + * to call {@link #dataBuffer()} to obtain the resulting encoded message. + * + * @param existing_bb The byte buffer to reuse. + * @param bb_factory The factory to be used for allocating a new internal buffer if + * the existing buffer needs to grow + */ + public FlatBufferBuilder(ByteBuffer existing_bb, ByteBufferFactory bb_factory) { + this(existing_bb.capacity(), bb_factory, existing_bb, Utf8.getDefault()); + } + + /** + * Alternative constructor allowing reuse of {@link ByteBuffer}s. The builder + * can still grow the buffer as necessary. User classes should make sure + * to call {@link #dataBuffer()} to obtain the resulting encoded message. + * + * @param existing_bb The byte buffer to reuse. + */ + public FlatBufferBuilder(ByteBuffer existing_bb) { + this(existing_bb, new HeapByteBufferFactory()); + } + + /** + * Alternative initializer that allows reusing this object on an existing + * `ByteBuffer`. This method resets the builder's internal state, but keeps + * objects that have been allocated for temporary storage. + * + * @param existing_bb The byte buffer to reuse. + * @param bb_factory The factory to be used for allocating a new internal buffer if + * the existing buffer needs to grow + * @return Returns `this`. + */ + public FlatBufferBuilder init(ByteBuffer existing_bb, ByteBufferFactory bb_factory){ + this.bb_factory = bb_factory; + bb = existing_bb; + bb.clear(); + bb.order(ByteOrder.LITTLE_ENDIAN); + minalign = 1; + space = bb.capacity(); + vtable_in_use = 0; + nested = false; + finished = false; + object_start = 0; + num_vtables = 0; + vector_num_elems = 0; + return this; + } + + /** + * An interface that provides a user of the FlatBufferBuilder class the ability to specify + * the method in which the internal buffer gets allocated. This allows for alternatives + * to the default behavior, which is to allocate memory for a new byte-array + * backed `ByteBuffer` array inside the JVM. + * + * The FlatBufferBuilder class contains the HeapByteBufferFactory class to + * preserve the default behavior in the event that the user does not provide + * their own implementation of this interface. + */ + public static abstract class ByteBufferFactory { + /** + * Create a `ByteBuffer` with a given capacity. + * The returned ByteBuf must have a ByteOrder.LITTLE_ENDIAN ByteOrder. + * + * @param capacity The size of the `ByteBuffer` to allocate. + * @return Returns the new `ByteBuffer` that was allocated. + */ + public abstract ByteBuffer newByteBuffer(int capacity); + + /** + * Release a ByteBuffer. Current {@link FlatBufferBuilder} + * released any reference to it, so it is safe to dispose the buffer + * or return it to a pool. + * It is not guaranteed that the buffer has been created + * with {@link #newByteBuffer(int) }. + * + * @param bb the buffer to release + */ + public void releaseByteBuffer(ByteBuffer bb) { + } + } + + /** + * An implementation of the ByteBufferFactory interface that is used when + * one is not provided by the user. + * + * Allocate memory for a new byte-array backed `ByteBuffer` array inside the JVM. + */ + public static final class HeapByteBufferFactory extends ByteBufferFactory { + + public static final HeapByteBufferFactory INSTANCE = new HeapByteBufferFactory(); + + @Override + public ByteBuffer newByteBuffer(int capacity) { + return ByteBuffer.allocate(capacity).order(ByteOrder.LITTLE_ENDIAN); + } + } + + /** + * Helper function to test if a field is present in the table + * + * @param table Flatbuffer table + * @param offset virtual table offset + * @return true if the filed is present + */ + public static boolean isFieldPresent(Table table, int offset) { + return table.__offset(offset) != 0; + } + + /** + * Reset the FlatBufferBuilder by purging all data that it holds. + */ + public void clear(){ + space = bb.capacity(); + bb.clear(); + minalign = 1; + while(vtable_in_use > 0) vtable[--vtable_in_use] = 0; + vtable_in_use = 0; + nested = false; + finished = false; + object_start = 0; + num_vtables = 0; + vector_num_elems = 0; + } + + /** + * Doubles the size of the backing {@link ByteBuffer} and copies the old data towards the + * end of the new buffer (since we build the buffer backwards). + * + * @param bb The current buffer with the existing data. + * @param bb_factory The factory to be used for allocating the new internal buffer + * @return A new byte buffer with the old data copied copied to it. The data is + * located at the end of the buffer. + */ + static ByteBuffer growByteBuffer(ByteBuffer bb, ByteBufferFactory bb_factory) { + int old_buf_size = bb.capacity(); + if ((old_buf_size & 0xC0000000) != 0) // Ensure we don't grow beyond what fits in an int. + throw new AssertionError("FlatBuffers: cannot grow buffer beyond 2 gigabytes."); + int new_buf_size = old_buf_size == 0 ? 1 : old_buf_size << 1; + bb.position(0); + ByteBuffer nbb = bb_factory.newByteBuffer(new_buf_size); + nbb.position(new_buf_size - old_buf_size); + nbb.put(bb); + return nbb; + } + + /** + * Offset relative to the end of the buffer. + * + * @return Offset relative to the end of the buffer. + */ + public int offset() { + return bb.capacity() - space; + } + + /** + * Add zero valued bytes to prepare a new entry to be added. + * + * @param byte_size Number of bytes to add. + */ + public void pad(int byte_size) { + for (int i = 0; i < byte_size; i++) bb.put(--space, (byte)0); + } + + /** + * Prepare to write an element of `size` after `additional_bytes` + * have been written, e.g. if you write a string, you need to align such + * the int length field is aligned to {@link com.google.flatbuffers.Constants#SIZEOF_INT}, and + * the string data follows it directly. If all you need to do is alignment, `additional_bytes` + * will be 0. + * + * @param size This is the of the new element to write. + * @param additional_bytes The padding size. + */ + public void prep(int size, int additional_bytes) { + // Track the biggest thing we've ever aligned to. + if (size > minalign) minalign = size; + // Find the amount of alignment needed such that `size` is properly + // aligned after `additional_bytes` + int align_size = ((~(bb.capacity() - space + additional_bytes)) + 1) & (size - 1); + // Reallocate the buffer if needed. + while (space < align_size + size + additional_bytes) { + int old_buf_size = bb.capacity(); + ByteBuffer old = bb; + bb = growByteBuffer(old, bb_factory); + if (old != bb) { + bb_factory.releaseByteBuffer(old); + } + space += bb.capacity() - old_buf_size; + } + pad(align_size); + } + + /** + * Add a `boolean` to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A `boolean` to put into the buffer. + */ + public void putBoolean(boolean x) { bb.put (space -= Constants.SIZEOF_BYTE, (byte)(x ? 1 : 0)); } + + /** + * Add a `byte` to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A `byte` to put into the buffer. + */ + public void putByte (byte x) { bb.put (space -= Constants.SIZEOF_BYTE, x); } + + /** + * Add a `short` to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A `short` to put into the buffer. + */ + public void putShort (short x) { bb.putShort (space -= Constants.SIZEOF_SHORT, x); } + + /** + * Add an `int` to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x An `int` to put into the buffer. + */ + public void putInt (int x) { bb.putInt (space -= Constants.SIZEOF_INT, x); } + + /** + * Add a `long` to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A `long` to put into the buffer. + */ + public void putLong (long x) { bb.putLong (space -= Constants.SIZEOF_LONG, x); } + + /** + * Add a `float` to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A `float` to put into the buffer. + */ + public void putFloat (float x) { bb.putFloat (space -= Constants.SIZEOF_FLOAT, x); } + + /** + * Add a `double` to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A `double` to put into the buffer. + */ + public void putDouble (double x) { bb.putDouble(space -= Constants.SIZEOF_DOUBLE, x); } + /// @endcond + + /** + * Add a `boolean` to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A `boolean` to put into the buffer. + */ + public void addBoolean(boolean x) { prep(Constants.SIZEOF_BYTE, 0); putBoolean(x); } + + /** + * Add a `byte` to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A `byte` to put into the buffer. + */ + public void addByte (byte x) { prep(Constants.SIZEOF_BYTE, 0); putByte (x); } + + /** + * Add a `short` to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A `short` to put into the buffer. + */ + public void addShort (short x) { prep(Constants.SIZEOF_SHORT, 0); putShort (x); } + + /** + * Add an `int` to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x An `int` to put into the buffer. + */ + public void addInt (int x) { prep(Constants.SIZEOF_INT, 0); putInt (x); } + + /** + * Add a `long` to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A `long` to put into the buffer. + */ + public void addLong (long x) { prep(Constants.SIZEOF_LONG, 0); putLong (x); } + + /** + * Add a `float` to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A `float` to put into the buffer. + */ + public void addFloat (float x) { prep(Constants.SIZEOF_FLOAT, 0); putFloat (x); } + + /** + * Add a `double` to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A `double` to put into the buffer. + */ + public void addDouble (double x) { prep(Constants.SIZEOF_DOUBLE, 0); putDouble (x); } + + /** + * Adds on offset, relative to where it will be written. + * + * @param off The offset to add. + */ + public void addOffset(int off) { + prep(SIZEOF_INT, 0); // Ensure alignment is already done. + assert off <= offset(); + off = offset() - off + SIZEOF_INT; + putInt(off); + } + + /// @cond FLATBUFFERS_INTERNAL + /** + * Start a new array/vector of objects. Users usually will not call + * this directly. The `FlatBuffers` compiler will create a start/end + * method for vector types in generated code. + *

+ * The expected sequence of calls is: + *

    + *
  1. Start the array using this method.
  2. + *
  3. Call {@link #addOffset(int)} `num_elems` number of times to set + * the offset of each element in the array.
  4. + *
  5. Call {@link #endVector()} to retrieve the offset of the array.
  6. + *
+ *

+ * For example, to create an array of strings, do: + *

{@code
+    * // Need 10 strings
+    * FlatBufferBuilder builder = new FlatBufferBuilder(existingBuffer);
+    * int[] offsets = new int[10];
+    *
+    * for (int i = 0; i < 10; i++) {
+    *   offsets[i] = fbb.createString(" " + i);
+    * }
+    *
+    * // Have the strings in the buffer, but don't have a vector.
+    * // Add a vector that references the newly created strings:
+    * builder.startVector(4, offsets.length, 4);
+    *
+    * // Add each string to the newly created vector
+    * // The strings are added in reverse order since the buffer
+    * // is filled in back to front
+    * for (int i = offsets.length - 1; i >= 0; i--) {
+    *   builder.addOffset(offsets[i]);
+    * }
+    *
+    * // Finish off the vector
+    * int offsetOfTheVector = fbb.endVector();
+    * }
+ * + * @param elem_size The size of each element in the array. + * @param num_elems The number of elements in the array. + * @param alignment The alignment of the array. + */ + public void startVector(int elem_size, int num_elems, int alignment) { + notNested(); + vector_num_elems = num_elems; + prep(SIZEOF_INT, elem_size * num_elems); + prep(alignment, elem_size * num_elems); // Just in case alignment > int. + nested = true; + } + + /** + * Finish off the creation of an array and all its elements. The array + * must be created with {@link #startVector(int, int, int)}. + * + * @return The offset at which the newly created array starts. + * @see #startVector(int, int, int) + */ + public int endVector() { + if (!nested) + throw new AssertionError("FlatBuffers: endVector called without startVector"); + nested = false; + putInt(vector_num_elems); + return offset(); + } + /// @endcond + + /** + * Create a new array/vector and return a ByteBuffer to be filled later. + * Call {@link #endVector} after this method to get an offset to the beginning + * of vector. + * + * @param elem_size the size of each element in bytes. + * @param num_elems number of elements in the vector. + * @param alignment byte alignment. + * @return ByteBuffer with position and limit set to the space allocated for the array. + */ + public ByteBuffer createUnintializedVector(int elem_size, int num_elems, int alignment) { + int length = elem_size * num_elems; + startVector(elem_size, num_elems, alignment); + + bb.position(space -= length); + + // Slice and limit the copy vector to point to the 'array' + ByteBuffer copy = bb.slice().order(ByteOrder.LITTLE_ENDIAN); + copy.limit(length); + return copy; + } + + /** + * Create a vector of tables. + * + * @param offsets Offsets of the tables. + * @return Returns offset of the vector. + */ + public int createVectorOfTables(int[] offsets) { + notNested(); + startVector(Constants.SIZEOF_INT, offsets.length, Constants.SIZEOF_INT); + for(int i = offsets.length - 1; i >= 0; i--) addOffset(offsets[i]); + return endVector(); + } + + /** + * Create a vector of sorted by the key tables. + * + * @param obj Instance of the table subclass. + * @param offsets Offsets of the tables. + * @return Returns offset of the sorted vector. + */ + public int createSortedVectorOfTables(T obj, int[] offsets) { + obj.sortTables(offsets, bb); + return createVectorOfTables(offsets); + } + + /** + * Encode the string `s` in the buffer using UTF-8. If {@code s} is + * already a {@link CharBuffer}, this method is allocation free. + * + * @param s The string to encode. + * @return The offset in the buffer where the encoded string starts. + */ + public int createString(CharSequence s) { + int length = utf8.encodedLength(s); + addByte((byte)0); + startVector(1, length, 1); + bb.position(space -= length); + utf8.encodeUtf8(s, bb); + return endVector(); + } + + /** + * Create a string in the buffer from an already encoded UTF-8 string in a ByteBuffer. + * + * @param s An already encoded UTF-8 string as a `ByteBuffer`. + * @return The offset in the buffer where the encoded string starts. + */ + public int createString(ByteBuffer s) { + int length = s.remaining(); + addByte((byte)0); + startVector(1, length, 1); + bb.position(space -= length); + bb.put(s); + return endVector(); + } + + /** + * Create a byte array in the buffer. + * + * @param arr A source array with data + * @return The offset in the buffer where the encoded array starts. + */ + public int createByteVector(byte[] arr) { + int length = arr.length; + startVector(1, length, 1); + bb.position(space -= length); + bb.put(arr); + return endVector(); + } + + /** + * Create a byte array in the buffer. + * + * @param arr a source array with data. + * @param offset the offset in the source array to start copying from. + * @param length the number of bytes to copy from the source array. + * @return The offset in the buffer where the encoded array starts. + */ + public int createByteVector(byte[] arr, int offset, int length) { + startVector(1, length, 1); + bb.position(space -= length); + bb.put(arr, offset, length); + return endVector(); + } + + /** + * Create a byte array in the buffer. + * + * The source {@link ByteBuffer} position is advanced by {@link ByteBuffer#remaining()} places + * after this call. + * + * @param byteBuffer A source {@link ByteBuffer} with data. + * @return The offset in the buffer where the encoded array starts. + */ + public int createByteVector(ByteBuffer byteBuffer) { + int length = byteBuffer.remaining(); + startVector(1, length, 1); + bb.position(space -= length); + bb.put(byteBuffer); + return endVector(); + } + + /// @cond FLATBUFFERS_INTERNAL + /** + * Should not be accessing the final buffer before it is finished. + */ + public void finished() { + if (!finished) + throw new AssertionError( + "FlatBuffers: you can only access the serialized buffer after it has been" + + " finished by FlatBufferBuilder.finish()."); + } + + /** + * Should not be creating any other object, string or vector + * while an object is being constructed. + */ + public void notNested() { + if (nested) + throw new AssertionError("FlatBuffers: object serialization must not be nested."); + } + + /** + * Structures are always stored inline, they need to be created right + * where they're used. You'll get this assertion failure if you + * created it elsewhere. + * + * @param obj The offset of the created object. + */ + public void Nested(int obj) { + if (obj != offset()) + throw new AssertionError("FlatBuffers: struct must be serialized inline."); + } + + /** + * Start encoding a new object in the buffer. Users will not usually need to + * call this directly. The `FlatBuffers` compiler will generate helper methods + * that call this method internally. + *

+ * For example, using the "Monster" code found on the "landing page". An + * object of type `Monster` can be created using the following code: + * + *

{@code
+    * int testArrayOfString = Monster.createTestarrayofstringVector(fbb, new int[] {
+    *   fbb.createString("test1"),
+    *   fbb.createString("test2")
+    * });
+    *
+    * Monster.startMonster(fbb);
+    * Monster.addPos(fbb, Vec3.createVec3(fbb, 1.0f, 2.0f, 3.0f, 3.0,
+    *   Color.Green, (short)5, (byte)6));
+    * Monster.addHp(fbb, (short)80);
+    * Monster.addName(fbb, str);
+    * Monster.addInventory(fbb, inv);
+    * Monster.addTestType(fbb, (byte)Any.Monster);
+    * Monster.addTest(fbb, mon2);
+    * Monster.addTest4(fbb, test4);
+    * Monster.addTestarrayofstring(fbb, testArrayOfString);
+    * int mon = Monster.endMonster(fbb);
+    * }
+ *

+ * Here: + *

    + *
  • The call to `Monster#startMonster(FlatBufferBuilder)` will call this + * method with the right number of fields set.
  • + *
  • `Monster#endMonster(FlatBufferBuilder)` will ensure {@link #endObject()} is called.
  • + *
+ *

+ * It's not recommended to call this method directly. If it's called manually, you must ensure + * to audit all calls to it whenever fields are added or removed from your schema. This is + * automatically done by the code generated by the `FlatBuffers` compiler. + * + * @param numfields The number of fields found in this object. + */ + public void startObject(int numfields) { + notNested(); + if (vtable == null || vtable.length < numfields) vtable = new int[numfields]; + vtable_in_use = numfields; + Arrays.fill(vtable, 0, vtable_in_use, 0); + nested = true; + object_start = offset(); + } + + /** + * Add a `boolean` to a table at `o` into its vtable, with value `x` and default `d`. + * + * @param o The index into the vtable. + * @param x A `boolean` to put into the buffer, depending on how defaults are handled. If + * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + * @param d A `boolean` default value to compare against when `force_defaults` is `false`. + */ + public void addBoolean(int o, boolean x, boolean d) { if(force_defaults || x != d) { addBoolean(x); slot(o); } } + + /** + * Add a `byte` to a table at `o` into its vtable, with value `x` and default `d`. + * + * @param o The index into the vtable. + * @param x A `byte` to put into the buffer, depending on how defaults are handled. If + * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + * @param d A `byte` default value to compare against when `force_defaults` is `false`. + */ + public void addByte (int o, byte x, int d) { if(force_defaults || x != d) { addByte (x); slot(o); } } + + /** + * Add a `short` to a table at `o` into its vtable, with value `x` and default `d`. + * + * @param o The index into the vtable. + * @param x A `short` to put into the buffer, depending on how defaults are handled. If + * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + * @param d A `short` default value to compare against when `force_defaults` is `false`. + */ + public void addShort (int o, short x, int d) { if(force_defaults || x != d) { addShort (x); slot(o); } } + + /** + * Add an `int` to a table at `o` into its vtable, with value `x` and default `d`. + * + * @param o The index into the vtable. + * @param x An `int` to put into the buffer, depending on how defaults are handled. If + * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + * @param d An `int` default value to compare against when `force_defaults` is `false`. + */ + public void addInt (int o, int x, int d) { if(force_defaults || x != d) { addInt (x); slot(o); } } + + /** + * Add a `long` to a table at `o` into its vtable, with value `x` and default `d`. + * + * @param o The index into the vtable. + * @param x A `long` to put into the buffer, depending on how defaults are handled. If + * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + * @param d A `long` default value to compare against when `force_defaults` is `false`. + */ + public void addLong (int o, long x, long d) { if(force_defaults || x != d) { addLong (x); slot(o); } } + + /** + * Add a `float` to a table at `o` into its vtable, with value `x` and default `d`. + * + * @param o The index into the vtable. + * @param x A `float` to put into the buffer, depending on how defaults are handled. If + * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + * @param d A `float` default value to compare against when `force_defaults` is `false`. + */ + public void addFloat (int o, float x, double d) { if(force_defaults || x != d) { addFloat (x); slot(o); } } + + /** + * Add a `double` to a table at `o` into its vtable, with value `x` and default `d`. + * + * @param o The index into the vtable. + * @param x A `double` to put into the buffer, depending on how defaults are handled. If + * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + * @param d A `double` default value to compare against when `force_defaults` is `false`. + */ + public void addDouble (int o, double x, double d) { if(force_defaults || x != d) { addDouble (x); slot(o); } } + + /** + * Add an `offset` to a table at `o` into its vtable, with value `x` and default `d`. + * + * @param o The index into the vtable. + * @param x An `offset` to put into the buffer, depending on how defaults are handled. If + * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + * @param d An `offset` default value to compare against when `force_defaults` is `false`. + */ + public void addOffset (int o, int x, int d) { if(force_defaults || x != d) { addOffset (x); slot(o); } } + + /** + * Add a struct to the table. Structs are stored inline, so nothing additional is being added. + * + * @param voffset The index into the vtable. + * @param x The offset of the created struct. + * @param d The default value is always `0`. + */ + public void addStruct(int voffset, int x, int d) { + if(x != d) { + Nested(x); + slot(voffset); + } + } + + /** + * Set the current vtable at `voffset` to the current location in the buffer. + * + * @param voffset The index into the vtable to store the offset relative to the end of the + * buffer. + */ + public void slot(int voffset) { + vtable[voffset] = offset(); + } + + /** + * Finish off writing the object that is under construction. + * + * @return The offset to the object inside {@link #dataBuffer()}. + * @see #startObject(int) + */ + public int endObject() { + if (vtable == null || !nested) + throw new AssertionError("FlatBuffers: endObject called without startObject"); + addInt(0); + int vtableloc = offset(); + // Write out the current vtable. + int i = vtable_in_use - 1; + // Trim trailing zeroes. + for (; i >= 0 && vtable[i] == 0; i--) {} + int trimmed_size = i + 1; + for (; i >= 0 ; i--) { + // Offset relative to the start of the table. + short off = (short)(vtable[i] != 0 ? vtableloc - vtable[i] : 0); + addShort(off); + } + + final int standard_fields = 2; // The fields below: + addShort((short)(vtableloc - object_start)); + addShort((short)((trimmed_size + standard_fields) * SIZEOF_SHORT)); + + // Search for an existing vtable that matches the current one. + int existing_vtable = 0; + outer_loop: + for (i = 0; i < num_vtables; i++) { + int vt1 = bb.capacity() - vtables[i]; + int vt2 = space; + short len = bb.getShort(vt1); + if (len == bb.getShort(vt2)) { + for (int j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) { + if (bb.getShort(vt1 + j) != bb.getShort(vt2 + j)) { + continue outer_loop; + } + } + existing_vtable = vtables[i]; + break outer_loop; + } + } + + if (existing_vtable != 0) { + // Found a match: + // Remove the current vtable. + space = bb.capacity() - vtableloc; + // Point table to existing vtable. + bb.putInt(space, existing_vtable - vtableloc); + } else { + // No match: + // Add the location of the current vtable to the list of vtables. + if (num_vtables == vtables.length) vtables = Arrays.copyOf(vtables, num_vtables * 2); + vtables[num_vtables++] = offset(); + // Point table to current vtable. + bb.putInt(bb.capacity() - vtableloc, offset() - vtableloc); + } + + nested = false; + return vtableloc; + } + + /** + * Checks that a required field has been set in a given table that has + * just been constructed. + * + * @param table The offset to the start of the table from the `ByteBuffer` capacity. + * @param field The offset to the field in the vtable. + */ + public void required(int table, int field) { + int table_start = bb.capacity() - table; + int vtable_start = table_start - bb.getInt(table_start); + boolean ok = bb.getShort(vtable_start + field) != 0; + // If this fails, the caller will show what field needs to be set. + if (!ok) + throw new AssertionError("FlatBuffers: field " + field + " must be set"); + } + /// @endcond + + /** + * Finalize a buffer, pointing to the given `root_table`. + * + * @param root_table An offset to be added to the buffer. + * @param size_prefix Whether to prefix the size to the buffer. + */ + protected void finish(int root_table, boolean size_prefix) { + prep(minalign, SIZEOF_INT + (size_prefix ? SIZEOF_INT : 0)); + addOffset(root_table); + if (size_prefix) { + addInt(bb.capacity() - space); + } + bb.position(space); + finished = true; + } + + /** + * Finalize a buffer, pointing to the given `root_table`. + * + * @param root_table An offset to be added to the buffer. + */ + public void finish(int root_table) { + finish(root_table, false); + } + + /** + * Finalize a buffer, pointing to the given `root_table`, with the size prefixed. + * + * @param root_table An offset to be added to the buffer. + */ + public void finishSizePrefixed(int root_table) { + finish(root_table, true); + } + + /** + * Finalize a buffer, pointing to the given `root_table`. + * + * @param root_table An offset to be added to the buffer. + * @param file_identifier A FlatBuffer file identifier to be added to the buffer before + * `root_table`. + * @param size_prefix Whether to prefix the size to the buffer. + */ + protected void finish(int root_table, String file_identifier, boolean size_prefix) { + prep(minalign, SIZEOF_INT + FILE_IDENTIFIER_LENGTH + (size_prefix ? SIZEOF_INT : 0)); + if (file_identifier.length() != FILE_IDENTIFIER_LENGTH) + throw new AssertionError("FlatBuffers: file identifier must be length " + + FILE_IDENTIFIER_LENGTH); + for (int i = FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) { + addByte((byte)file_identifier.charAt(i)); + } + finish(root_table, size_prefix); + } + + /** + * Finalize a buffer, pointing to the given `root_table`. + * + * @param root_table An offset to be added to the buffer. + * @param file_identifier A FlatBuffer file identifier to be added to the buffer before + * `root_table`. + */ + public void finish(int root_table, String file_identifier) { + finish(root_table, file_identifier, false); + } + + /** + * Finalize a buffer, pointing to the given `root_table`, with the size prefixed. + * + * @param root_table An offset to be added to the buffer. + * @param file_identifier A FlatBuffer file identifier to be added to the buffer before + * `root_table`. + */ + public void finishSizePrefixed(int root_table, String file_identifier) { + finish(root_table, file_identifier, true); + } + + /** + * In order to save space, fields that are set to their default value + * don't get serialized into the buffer. Forcing defaults provides a + * way to manually disable this optimization. + * + * @param forceDefaults When set to `true`, always serializes default values. + * @return Returns `this`. + */ + public FlatBufferBuilder forceDefaults(boolean forceDefaults){ + this.force_defaults = forceDefaults; + return this; + } + + /** + * Get the ByteBuffer representing the FlatBuffer. Only call this after you've + * called `finish()`. The actual data starts at the ByteBuffer's current position, + * not necessarily at `0`. + * + * @return The {@link ByteBuffer} representing the FlatBuffer + */ + public ByteBuffer dataBuffer() { + finished(); + return bb; + } + + /** + * The FlatBuffer data doesn't start at offset 0 in the {@link ByteBuffer}, but + * now the {@code ByteBuffer}'s position is set to that location upon {@link #finish(int)}. + * + * @return The {@link ByteBuffer#position() position} the data starts in {@link #dataBuffer()} + * @deprecated This method should not be needed anymore, but is left + * here for the moment to document this API change. It will be removed in the future. + */ + @Deprecated + private int dataStart() { + finished(); + return space; + } + + /** + * A utility function to copy and return the ByteBuffer data from `start` to + * `start` + `length` as a `byte[]`. + * + * @param start Start copying at this offset. + * @param length How many bytes to copy. + * @return A range copy of the {@link #dataBuffer() data buffer}. + * @throws IndexOutOfBoundsException If the range of bytes is ouf of bound. + */ + public byte[] sizedByteArray(int start, int length){ + finished(); + byte[] array = new byte[length]; + bb.position(start); + bb.get(array); + return array; + } + + /** + * A utility function to copy and return the ByteBuffer data as a `byte[]`. + * + * @return A full copy of the {@link #dataBuffer() data buffer}. + */ + public byte[] sizedByteArray() { + return sizedByteArray(space, bb.capacity() - space); + } + + /** + * A utility function to return an InputStream to the ByteBuffer data + * + * @return An InputStream that starts at the beginning of the ByteBuffer data + * and can read to the end of it. + */ + public InputStream sizedInputStream() { + finished(); + ByteBuffer duplicate = bb.duplicate(); + duplicate.position(space); + duplicate.limit(bb.capacity()); + return new ByteBufferBackedInputStream(duplicate); + } + + /** + * A class that allows a user to create an InputStream from a ByteBuffer. + */ + static class ByteBufferBackedInputStream extends InputStream { + + ByteBuffer buf; + + public ByteBufferBackedInputStream(ByteBuffer buf) { + this.buf = buf; + } + + public int read() throws IOException { + try { + return buf.get() & 0xFF; + } catch(BufferUnderflowException e) { + return -1; + } + } + } + +} + +/// @} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Struct.java b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Struct.java new file mode 100644 index 0000000..39a8215 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Struct.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * 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. + */ + +package com.google.flatbuffers; + +import java.nio.ByteBuffer; + +/// @cond FLATBUFFERS_INTERNAL + +/** + * All structs in the generated code derive from this class, and add their own accessors. + */ +public class Struct { + /** Used to hold the position of the `bb` buffer. */ + protected int bb_pos; + /** The underlying ByteBuffer to hold the data of the Struct. */ + protected ByteBuffer bb; + + /** + * Resets internal state with a null {@code ByteBuffer} and a zero position. + * + * This method exists primarily to allow recycling Struct instances without risking memory leaks + * due to {@code ByteBuffer} references. The instance will be unusable until it is assigned + * again to a {@code ByteBuffer}. + * + * @param struct the instance to reset to initial state + */ + public void __reset() { + bb = null; + bb_pos = 0; + } +} + +/// @endcond diff --git a/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Table.java b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Table.java new file mode 100644 index 0000000..cedbb8e --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Table.java @@ -0,0 +1,281 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * 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. + */ + +package com.google.flatbuffers; + +import static com.google.flatbuffers.Constants.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; + +/// @cond FLATBUFFERS_INTERNAL + +/** + * All tables in the generated code derive from this class, and add their own accessors. + */ +public class Table { + public final static ThreadLocal UTF8_CHARSET = new ThreadLocal() { + @Override + protected Charset initialValue() { + return Charset.forName("UTF-8"); + } + }; + /** Used to hold the position of the `bb` buffer. */ + protected int bb_pos; + /** The underlying ByteBuffer to hold the data of the Table. */ + protected ByteBuffer bb; + /** Used to hold the vtable position. */ + protected int vtable_start; + /** Used to hold the vtable size. */ + protected int vtable_size; + Utf8 utf8 = Utf8.getDefault(); + + /** + * Get the underlying ByteBuffer. + * + * @return Returns the Table's ByteBuffer. + */ + public ByteBuffer getByteBuffer() { return bb; } + + /** + * Look up a field in the vtable. + * + * @param vtable_offset An `int` offset to the vtable in the Table's ByteBuffer. + * @return Returns an offset into the object, or `0` if the field is not present. + */ + protected int __offset(int vtable_offset) { + return vtable_offset < vtable_size ? bb.getShort(vtable_start + vtable_offset) : 0; + } + + protected static int __offset(int vtable_offset, int offset, ByteBuffer bb) { + int vtable = bb.capacity() - offset; + return bb.getShort(vtable + vtable_offset - bb.getInt(vtable)) + vtable; + } + + /** + * Retrieve a relative offset. + * + * @param offset An `int` index into the Table's ByteBuffer containing the relative offset. + * @return Returns the relative offset stored at `offset`. + */ + protected int __indirect(int offset) { + return offset + bb.getInt(offset); + } + + protected static int __indirect(int offset, ByteBuffer bb) { + return offset + bb.getInt(offset); + } + + /** + * Create a Java `String` from UTF-8 data stored inside the FlatBuffer. + * + * This allocates a new string and converts to wide chars upon each access, + * which is not very efficient. Instead, each FlatBuffer string also comes with an + * accessor based on __vector_as_bytebuffer below, which is much more efficient, + * assuming your Java program can handle UTF-8 data directly. + * + * @param offset An `int` index into the Table's ByteBuffer. + * @return Returns a `String` from the data stored inside the FlatBuffer at `offset`. + */ + protected String __string(int offset) { + offset += bb.getInt(offset); + int length = bb.getInt(offset); + return utf8.decodeUtf8(bb, offset + SIZEOF_INT, length); + } + + /** + * Get the length of a vector. + * + * @param offset An `int` index into the Table's ByteBuffer. + * @return Returns the length of the vector whose offset is stored at `offset`. + */ + protected int __vector_len(int offset) { + offset += bb_pos; + offset += bb.getInt(offset); + return bb.getInt(offset); + } + + /** + * Get the start data of a vector. + * + * @param offset An `int` index into the Table's ByteBuffer. + * @return Returns the start of the vector data whose offset is stored at `offset`. + */ + protected int __vector(int offset) { + offset += bb_pos; + return offset + bb.getInt(offset) + SIZEOF_INT; // data starts after the length + } + + /** + * Get a whole vector as a ByteBuffer. + * + * This is efficient, since it only allocates a new {@link ByteBuffer} object, + * but does not actually copy the data, it still refers to the same bytes + * as the original ByteBuffer. Also useful with nested FlatBuffers, etc. + * + * @param vector_offset The position of the vector in the byte buffer + * @param elem_size The size of each element in the array + * @return The {@link ByteBuffer} for the array + */ + protected ByteBuffer __vector_as_bytebuffer(int vector_offset, int elem_size) { + int o = __offset(vector_offset); + if (o == 0) return null; + ByteBuffer bb = this.bb.duplicate().order(ByteOrder.LITTLE_ENDIAN); + int vectorstart = __vector(o); + bb.position(vectorstart); + bb.limit(vectorstart + __vector_len(o) * elem_size); + return bb; + } + + /** + * Initialize vector as a ByteBuffer. + * + * This is more efficient than using duplicate, since it doesn't copy the data + * nor allocattes a new {@link ByteBuffer}, creating no garbage to be collected. + * + * @param bb The {@link ByteBuffer} for the array + * @param vector_offset The position of the vector in the byte buffer + * @param elem_size The size of each element in the array + * @return The {@link ByteBuffer} for the array + */ + protected ByteBuffer __vector_in_bytebuffer(ByteBuffer bb, int vector_offset, int elem_size) { + int o = this.__offset(vector_offset); + if (o == 0) return null; + int vectorstart = __vector(o); + bb.rewind(); + bb.limit(vectorstart + __vector_len(o) * elem_size); + bb.position(vectorstart); + return bb; + } + + /** + * Initialize any Table-derived type to point to the union at the given `offset`. + * + * @param t A `Table`-derived type that should point to the union at `offset`. + * @param offset An `int` index into the Table's ByteBuffer. + * @return Returns the Table that points to the union at `offset`. + */ + protected Table __union(Table t, int offset) { + offset += bb_pos; + t.bb_pos = offset + bb.getInt(offset); + t.bb = bb; + t.vtable_start = t.bb_pos - bb.getInt(t.bb_pos); + t.vtable_size = bb.getShort(t.vtable_start); + return t; + } + + /** + * Check if a {@link ByteBuffer} contains a file identifier. + * + * @param bb A {@code ByteBuffer} to check if it contains the identifier + * `ident`. + * @param ident A `String` identifier of the FlatBuffer file. + * @return True if the buffer contains the file identifier + */ + protected static boolean __has_identifier(ByteBuffer bb, String ident) { + if (ident.length() != FILE_IDENTIFIER_LENGTH) + throw new AssertionError("FlatBuffers: file identifier must be length " + + FILE_IDENTIFIER_LENGTH); + for (int i = 0; i < FILE_IDENTIFIER_LENGTH; i++) { + if (ident.charAt(i) != (char)bb.get(bb.position() + SIZEOF_INT + i)) return false; + } + return true; + } + + /** + * Sort tables by the key. + * + * @param offsets An 'int' indexes of the tables into the bb. + * @param bb A {@code ByteBuffer} to get the tables. + */ + protected void sortTables(int[] offsets, final ByteBuffer bb) { + Integer[] off = new Integer[offsets.length]; + for (int i = 0; i < offsets.length; i++) off[i] = offsets[i]; + java.util.Arrays.sort(off, new java.util.Comparator() { + public int compare(Integer o1, Integer o2) { + return keysCompare(o1, o2, bb); + } + }); + for (int i = 0; i < offsets.length; i++) offsets[i] = off[i]; + } + + /** + * Compare two tables by the key. + * + * @param o1 An 'Integer' index of the first key into the bb. + * @param o2 An 'Integer' index of the second key into the bb. + * @param bb A {@code ByteBuffer} to get the keys. + */ + protected int keysCompare(Integer o1, Integer o2, ByteBuffer bb) { return 0; } + + /** + * Compare two strings in the buffer. + * + * @param offset_1 An 'int' index of the first string into the bb. + * @param offset_2 An 'int' index of the second string into the bb. + * @param bb A {@code ByteBuffer} to get the strings. + */ + protected static int compareStrings(int offset_1, int offset_2, ByteBuffer bb) { + offset_1 += bb.getInt(offset_1); + offset_2 += bb.getInt(offset_2); + int len_1 = bb.getInt(offset_1); + int len_2 = bb.getInt(offset_2); + int startPos_1 = offset_1 + SIZEOF_INT; + int startPos_2 = offset_2 + SIZEOF_INT; + int len = Math.min(len_1, len_2); + for(int i = 0; i < len; i++) { + if (bb.get(i + startPos_1) != bb.get(i + startPos_2)) + return bb.get(i + startPos_1) - bb.get(i + startPos_2); + } + return len_1 - len_2; + } + + /** + * Compare string from the buffer with the 'String' object. + * + * @param offset_1 An 'int' index of the first string into the bb. + * @param key Second string as a byte array. + * @param bb A {@code ByteBuffer} to get the first string. + */ + protected static int compareStrings(int offset_1, byte[] key, ByteBuffer bb) { + offset_1 += bb.getInt(offset_1); + int len_1 = bb.getInt(offset_1); + int len_2 = key.length; + int startPos_1 = offset_1 + Constants.SIZEOF_INT; + int len = Math.min(len_1, len_2); + for (int i = 0; i < len; i++) { + if (bb.get(i + startPos_1) != key[i]) + return bb.get(i + startPos_1) - key[i]; + } + return len_1 - len_2; + } + + /** + * Resets the internal state with a null {@code ByteBuffer} and a zero position. + * + * This method exists primarily to allow recycling Table instances without risking memory leaks + * due to {@code ByteBuffer} references. The instance will be unusable until it is assigned + * again to a {@code ByteBuffer}. + */ + public void __reset() { + bb = null; + bb_pos = 0; + vtable_start = 0; + vtable_size = 0; + } +} + +/// @endcond diff --git a/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Utf8.java b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Utf8.java new file mode 100644 index 0000000..efb6811 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Utf8.java @@ -0,0 +1,193 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * 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. + */ + +package com.google.flatbuffers; + +import java.nio.ByteBuffer; + +import static java.lang.Character.MIN_HIGH_SURROGATE; +import static java.lang.Character.MIN_LOW_SURROGATE; +import static java.lang.Character.MIN_SUPPLEMENTARY_CODE_POINT; + +public abstract class Utf8 { + + /** + * Returns the number of bytes in the UTF-8-encoded form of {@code sequence}. For a string, + * this method is equivalent to {@code string.getBytes(UTF_8).length}, but is more efficient in + * both time and space. + * + * @throws IllegalArgumentException if {@code sequence} contains ill-formed UTF-16 (unpaired + * surrogates) + */ + public abstract int encodedLength(CharSequence sequence); + + /** + * Encodes the given characters to the target {@link ByteBuffer} using UTF-8 encoding. + * + *

Selects an optimal algorithm based on the type of {@link ByteBuffer} (i.e. heap or direct) + * and the capabilities of the platform. + * + * @param in the source string to be encoded + * @param out the target buffer to receive the encoded string. + */ + public abstract void encodeUtf8(CharSequence in, ByteBuffer out); + + /** + * Decodes the given UTF-8 portion of the {@link ByteBuffer} into a {@link String}. + * + * @throws IllegalArgumentException if the input is not valid UTF-8. + */ + public abstract String decodeUtf8(ByteBuffer buffer, int offset, int length); + + private static Utf8 DEFAULT; + + /** + * Get the default UTF-8 processor. + * @return the default processor + */ + public static Utf8 getDefault() { + if (DEFAULT == null) { + DEFAULT = new Utf8Safe(); + } + return DEFAULT; + } + + /** + * Set the default instance of the UTF-8 processor. + * @param instance the new instance to use + */ + public static void setDefault(Utf8 instance) { + DEFAULT = instance; + } + + /** + * Utility methods for decoding bytes into {@link String}. Callers are responsible for extracting + * bytes (possibly using Unsafe methods), and checking remaining bytes. All other UTF-8 validity + * checks and codepoint conversion happen in this class. + */ + static class DecodeUtil { + + /** + * Returns whether this is a single-byte codepoint (i.e., ASCII) with the form '0XXXXXXX'. + */ + static boolean isOneByte(byte b) { + return b >= 0; + } + + /** + * Returns whether this is a two-byte codepoint with the form '10XXXXXX'. + */ + static boolean isTwoBytes(byte b) { + return b < (byte) 0xE0; + } + + /** + * Returns whether this is a three-byte codepoint with the form '110XXXXX'. + */ + static boolean isThreeBytes(byte b) { + return b < (byte) 0xF0; + } + + static void handleOneByte(byte byte1, char[] resultArr, int resultPos) { + resultArr[resultPos] = (char) byte1; + } + + static void handleTwoBytes( + byte byte1, byte byte2, char[] resultArr, int resultPos) + throws IllegalArgumentException { + // Simultaneously checks for illegal trailing-byte in leading position (<= '11000000') and + // overlong 2-byte, '11000001'. + if (byte1 < (byte) 0xC2) { + throw new IllegalArgumentException("Invalid UTF-8: Illegal leading byte in 2 bytes utf"); + } + if (isNotTrailingByte(byte2)) { + throw new IllegalArgumentException("Invalid UTF-8: Illegal trailing byte in 2 bytes utf"); + } + resultArr[resultPos] = (char) (((byte1 & 0x1F) << 6) | trailingByteValue(byte2)); + } + + static void handleThreeBytes( + byte byte1, byte byte2, byte byte3, char[] resultArr, int resultPos) + throws IllegalArgumentException { + if (isNotTrailingByte(byte2) + // overlong? 5 most significant bits must not all be zero + || (byte1 == (byte) 0xE0 && byte2 < (byte) 0xA0) + // check for illegal surrogate codepoints + || (byte1 == (byte) 0xED && byte2 >= (byte) 0xA0) + || isNotTrailingByte(byte3)) { + throw new IllegalArgumentException("Invalid UTF-8"); + } + resultArr[resultPos] = (char) + (((byte1 & 0x0F) << 12) | (trailingByteValue(byte2) << 6) | trailingByteValue(byte3)); + } + + static void handleFourBytes( + byte byte1, byte byte2, byte byte3, byte byte4, char[] resultArr, int resultPos) + throws IllegalArgumentException{ + if (isNotTrailingByte(byte2) + // Check that 1 <= plane <= 16. Tricky optimized form of: + // valid 4-byte leading byte? + // if (byte1 > (byte) 0xF4 || + // overlong? 4 most significant bits must not all be zero + // byte1 == (byte) 0xF0 && byte2 < (byte) 0x90 || + // codepoint larger than the highest code point (U+10FFFF)? + // byte1 == (byte) 0xF4 && byte2 > (byte) 0x8F) + || (((byte1 << 28) + (byte2 - (byte) 0x90)) >> 30) != 0 + || isNotTrailingByte(byte3) + || isNotTrailingByte(byte4)) { + throw new IllegalArgumentException("Invalid UTF-8"); + } + int codepoint = ((byte1 & 0x07) << 18) + | (trailingByteValue(byte2) << 12) + | (trailingByteValue(byte3) << 6) + | trailingByteValue(byte4); + resultArr[resultPos] = DecodeUtil.highSurrogate(codepoint); + resultArr[resultPos + 1] = DecodeUtil.lowSurrogate(codepoint); + } + + /** + * Returns whether the byte is not a valid continuation of the form '10XXXXXX'. + */ + private static boolean isNotTrailingByte(byte b) { + return b > (byte) 0xBF; + } + + /** + * Returns the actual value of the trailing byte (removes the prefix '10') for composition. + */ + private static int trailingByteValue(byte b) { + return b & 0x3F; + } + + private static char highSurrogate(int codePoint) { + return (char) ((MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT >>> 10)) + + (codePoint >>> 10)); + } + + private static char lowSurrogate(int codePoint) { + return (char) (MIN_LOW_SURROGATE + (codePoint & 0x3ff)); + } + } + + // These UTF-8 handling methods are copied from Guava's Utf8Unsafe class with a modification to throw + // a protocol buffer local exception. This exception is then caught in CodedOutputStream so it can + // fallback to more lenient behavior. + static class UnpairedSurrogateException extends IllegalArgumentException { + UnpairedSurrogateException(int index, int length) { + super("Unpaired surrogate at index " + index + " of " + length); + } + } +} diff --git a/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Utf8Safe.java b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Utf8Safe.java new file mode 100644 index 0000000..b2ebd01 --- /dev/null +++ b/sceneformsrc/sceneform/src/main/java/com/google/flatbuffers/Utf8Safe.java @@ -0,0 +1,451 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.google.flatbuffers; + +import java.nio.ByteBuffer; +import static java.lang.Character.MAX_SURROGATE; +import static java.lang.Character.MIN_SUPPLEMENTARY_CODE_POINT; +import static java.lang.Character.MIN_SURROGATE; +import static java.lang.Character.isSurrogatePair; +import static java.lang.Character.toCodePoint; + +/** + * A set of low-level, high-performance static utility methods related + * to the UTF-8 character encoding. This class has no dependencies + * outside of the core JDK libraries. + * + *

There are several variants of UTF-8. The one implemented by + * this class is the restricted definition of UTF-8 introduced in + * Unicode 3.1, which mandates the rejection of "overlong" byte + * sequences as well as rejection of 3-byte surrogate codepoint byte + * sequences. Note that the UTF-8 decoder included in Oracle's JDK + * has been modified to also reject "overlong" byte sequences, but (as + * of 2011) still accepts 3-byte surrogate codepoint byte sequences. + * + *

The byte sequences considered valid by this class are exactly + * those that can be roundtrip converted to Strings and back to bytes + * using the UTF-8 charset, without loss:

 {@code
+ * Arrays.equals(bytes, new String(bytes, Internal.UTF_8).getBytes(Internal.UTF_8))
+ * }
+ * + *

See the Unicode Standard,
+ * Table 3-6. UTF-8 Bit Distribution,
+ * Table 3-7. Well Formed UTF-8 Byte Sequences. + */ +final public class Utf8Safe extends Utf8 { + + /** + * Returns the number of bytes in the UTF-8-encoded form of {@code sequence}. For a string, + * this method is equivalent to {@code string.getBytes(UTF_8).length}, but is more efficient in + * both time and space. + * + * @throws IllegalArgumentException if {@code sequence} contains ill-formed UTF-16 (unpaired + * surrogates) + */ + private static int computeEncodedLength(CharSequence sequence) { + // Warning to maintainers: this implementation is highly optimized. + int utf16Length = sequence.length(); + int utf8Length = utf16Length; + int i = 0; + + // This loop optimizes for pure ASCII. + while (i < utf16Length && sequence.charAt(i) < 0x80) { + i++; + } + + // This loop optimizes for chars less than 0x800. + for (; i < utf16Length; i++) { + char c = sequence.charAt(i); + if (c < 0x800) { + utf8Length += ((0x7f - c) >>> 31); // branch free! + } else { + utf8Length += encodedLengthGeneral(sequence, i); + break; + } + } + + if (utf8Length < utf16Length) { + // Necessary and sufficient condition for overflow because of maximum 3x expansion + throw new IllegalArgumentException("UTF-8 length does not fit in int: " + + (utf8Length + (1L << 32))); + } + return utf8Length; + } + + private static int encodedLengthGeneral(CharSequence sequence, int start) { + int utf16Length = sequence.length(); + int utf8Length = 0; + for (int i = start; i < utf16Length; i++) { + char c = sequence.charAt(i); + if (c < 0x800) { + utf8Length += (0x7f - c) >>> 31; // branch free! + } else { + utf8Length += 2; + // jdk7+: if (Character.isSurrogate(c)) { + if (Character.MIN_SURROGATE <= c && c <= Character.MAX_SURROGATE) { + // Check that we have a well-formed surrogate pair. + int cp = Character.codePointAt(sequence, i); + if (cp < MIN_SUPPLEMENTARY_CODE_POINT) { + throw new Utf8Safe.UnpairedSurrogateException(i, utf16Length); + } + i++; + } + } + } + return utf8Length; + } + + private static String decodeUtf8Array(byte[] bytes, int index, int size) { + // Bitwise OR combines the sign bits so any negative value fails the check. + if ((index | size | bytes.length - index - size) < 0) { + throw new ArrayIndexOutOfBoundsException( + String.format("buffer length=%d, index=%d, size=%d", bytes.length, index, size)); + } + + int offset = index; + final int limit = offset + size; + + // The longest possible resulting String is the same as the number of input bytes, when it is + // all ASCII. For other cases, this over-allocates and we will truncate in the end. + char[] resultArr = new char[size]; + int resultPos = 0; + + // Optimize for 100% ASCII (Hotspot loves small simple top-level loops like this). + // This simple loop stops when we encounter a byte >= 0x80 (i.e. non-ASCII). + while (offset < limit) { + byte b = bytes[offset]; + if (!DecodeUtil.isOneByte(b)) { + break; + } + offset++; + DecodeUtil.handleOneByte(b, resultArr, resultPos++); + } + + while (offset < limit) { + byte byte1 = bytes[offset++]; + if (DecodeUtil.isOneByte(byte1)) { + DecodeUtil.handleOneByte(byte1, resultArr, resultPos++); + // It's common for there to be multiple ASCII characters in a run mixed in, so add an + // extra optimized loop to take care of these runs. + while (offset < limit) { + byte b = bytes[offset]; + if (!DecodeUtil.isOneByte(b)) { + break; + } + offset++; + DecodeUtil.handleOneByte(b, resultArr, resultPos++); + } + } else if (DecodeUtil.isTwoBytes(byte1)) { + if (offset >= limit) { + throw new IllegalArgumentException("Invalid UTF-8"); + } + DecodeUtil.handleTwoBytes(byte1, /* byte2 */ bytes[offset++], resultArr, resultPos++); + } else if (DecodeUtil.isThreeBytes(byte1)) { + if (offset >= limit - 1) { + throw new IllegalArgumentException("Invalid UTF-8"); + } + DecodeUtil.handleThreeBytes( + byte1, + /* byte2 */ bytes[offset++], + /* byte3 */ bytes[offset++], + resultArr, + resultPos++); + } else { + if (offset >= limit - 2) { + throw new IllegalArgumentException("Invalid UTF-8"); + } + DecodeUtil.handleFourBytes( + byte1, + /* byte2 */ bytes[offset++], + /* byte3 */ bytes[offset++], + /* byte4 */ bytes[offset++], + resultArr, + resultPos++); + // 4-byte case requires two chars. + resultPos++; + } + } + + return new String(resultArr, 0, resultPos); + } + + private static String decodeUtf8Buffer(ByteBuffer buffer, int offset, + int length) { + // Bitwise OR combines the sign bits so any negative value fails the check. + if ((offset | length | buffer.limit() - offset - length) < 0) { + throw new ArrayIndexOutOfBoundsException( + String.format("buffer limit=%d, index=%d, limit=%d", buffer.limit(), + offset, length)); + } + + final int limit = offset + length; + + // The longest possible resulting String is the same as the number of input bytes, when it is + // all ASCII. For other cases, this over-allocates and we will truncate in the end. + char[] resultArr = new char[length]; + int resultPos = 0; + + // Optimize for 100% ASCII (Hotspot loves small simple top-level loops like this). + // This simple loop stops when we encounter a byte >= 0x80 (i.e. non-ASCII). + while (offset < limit) { + byte b = buffer.get(offset); + if (!DecodeUtil.isOneByte(b)) { + break; + } + offset++; + DecodeUtil.handleOneByte(b, resultArr, resultPos++); + } + + while (offset < limit) { + byte byte1 = buffer.get(offset++); + if (DecodeUtil.isOneByte(byte1)) { + DecodeUtil.handleOneByte(byte1, resultArr, resultPos++); + // It's common for there to be multiple ASCII characters in a run mixed in, so add an + // extra optimized loop to take care of these runs. + while (offset < limit) { + byte b = buffer.get(offset); + if (!DecodeUtil.isOneByte(b)) { + break; + } + offset++; + DecodeUtil.handleOneByte(b, resultArr, resultPos++); + } + } else if (DecodeUtil.isTwoBytes(byte1)) { + if (offset >= limit) { + throw new IllegalArgumentException("Invalid UTF-8"); + } + DecodeUtil.handleTwoBytes( + byte1, /* byte2 */ buffer.get(offset++), resultArr, resultPos++); + } else if (DecodeUtil.isThreeBytes(byte1)) { + if (offset >= limit - 1) { + throw new IllegalArgumentException("Invalid UTF-8"); + } + DecodeUtil.handleThreeBytes( + byte1, + /* byte2 */ buffer.get(offset++), + /* byte3 */ buffer.get(offset++), + resultArr, + resultPos++); + } else { + if (offset >= limit - 2) { + throw new IllegalArgumentException("Invalid UTF-8"); + } + DecodeUtil.handleFourBytes( + byte1, + /* byte2 */ buffer.get(offset++), + /* byte3 */ buffer.get(offset++), + /* byte4 */ buffer.get(offset++), + resultArr, + resultPos++); + // 4-byte case requires two chars. + resultPos++; + } + } + + return new String(resultArr, 0, resultPos); + } + + @Override + public int encodedLength(CharSequence in) { + return computeEncodedLength(in); + } + + /** + * Decodes the given UTF-8 portion of the {@link ByteBuffer} into a {@link String}. + * + * @throws IllegalArgumentException if the input is not valid UTF-8. + */ + @Override + public String decodeUtf8(ByteBuffer buffer, int offset, int length) + throws IllegalArgumentException { + if (buffer.hasArray()) { + return decodeUtf8Array(buffer.array(), buffer.arrayOffset() + offset, length); + } else { + return decodeUtf8Buffer(buffer, offset, length); + } + } + + + private static void encodeUtf8Buffer(CharSequence in, ByteBuffer out) { + final int inLength = in.length(); + int outIx = out.position(); + int inIx = 0; + + // Since ByteBuffer.putXXX() already checks boundaries for us, no need to explicitly check + // access. Assume the buffer is big enough and let it handle the out of bounds exception + // if it occurs. + try { + // Designed to take advantage of + // https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination + for (char c; inIx < inLength && (c = in.charAt(inIx)) < 0x80; ++inIx) { + out.put(outIx + inIx, (byte) c); + } + if (inIx == inLength) { + // Successfully encoded the entire string. + out.position(outIx + inIx); + return; + } + + outIx += inIx; + for (char c; inIx < inLength; ++inIx, ++outIx) { + c = in.charAt(inIx); + if (c < 0x80) { + // One byte (0xxx xxxx) + out.put(outIx, (byte) c); + } else if (c < 0x800) { + // Two bytes (110x xxxx 10xx xxxx) + + // Benchmarks show put performs better than putShort here (for HotSpot). + out.put(outIx++, (byte) (0xC0 | (c >>> 6))); + out.put(outIx, (byte) (0x80 | (0x3F & c))); + } else if (c < MIN_SURROGATE || MAX_SURROGATE < c) { + // Three bytes (1110 xxxx 10xx xxxx 10xx xxxx) + // Maximum single-char code point is 0xFFFF, 16 bits. + + // Benchmarks show put performs better than putShort here (for HotSpot). + out.put(outIx++, (byte) (0xE0 | (c >>> 12))); + out.put(outIx++, (byte) (0x80 | (0x3F & (c >>> 6)))); + out.put(outIx, (byte) (0x80 | (0x3F & c))); + } else { + // Four bytes (1111 xxxx 10xx xxxx 10xx xxxx 10xx xxxx) + + // Minimum code point represented by a surrogate pair is 0x10000, 17 bits, four UTF-8 + // bytes + final char low; + if (inIx + 1 == inLength || !isSurrogatePair(c, (low = in.charAt(++inIx)))) { + throw new UnpairedSurrogateException(inIx, inLength); + } + // TODO: Consider using putInt() to improve performance. + int codePoint = toCodePoint(c, low); + out.put(outIx++, (byte) ((0xF << 4) | (codePoint >>> 18))); + out.put(outIx++, (byte) (0x80 | (0x3F & (codePoint >>> 12)))); + out.put(outIx++, (byte) (0x80 | (0x3F & (codePoint >>> 6)))); + out.put(outIx, (byte) (0x80 | (0x3F & codePoint))); + } + } + + // Successfully encoded the entire string. + out.position(outIx); + } catch (IndexOutOfBoundsException e) { + // TODO: Consider making the API throw IndexOutOfBoundsException instead. + + // If we failed in the outer ASCII loop, outIx will not have been updated. In this case, + // use inIx to determine the bad write index. + int badWriteIndex = out.position() + Math.max(inIx, outIx - out.position() + 1); + throw new ArrayIndexOutOfBoundsException( + "Failed writing " + in.charAt(inIx) + " at index " + badWriteIndex); + } + } + + private static int encodeUtf8Array(CharSequence in, byte[] out, + int offset, int length) { + int utf16Length = in.length(); + int j = offset; + int i = 0; + int limit = offset + length; + // Designed to take advantage of + // https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination + for (char c; i < utf16Length && i + j < limit && (c = in.charAt(i)) < 0x80; i++) { + out[j + i] = (byte) c; + } + if (i == utf16Length) { + return j + utf16Length; + } + j += i; + for (char c; i < utf16Length; i++) { + c = in.charAt(i); + if (c < 0x80 && j < limit) { + out[j++] = (byte) c; + } else if (c < 0x800 && j <= limit - 2) { // 11 bits, two UTF-8 bytes + out[j++] = (byte) ((0xF << 6) | (c >>> 6)); + out[j++] = (byte) (0x80 | (0x3F & c)); + } else if ((c < Character.MIN_SURROGATE || Character.MAX_SURROGATE < c) && j <= limit - 3) { + // Maximum single-char code point is 0xFFFF, 16 bits, three UTF-8 bytes + out[j++] = (byte) ((0xF << 5) | (c >>> 12)); + out[j++] = (byte) (0x80 | (0x3F & (c >>> 6))); + out[j++] = (byte) (0x80 | (0x3F & c)); + } else if (j <= limit - 4) { + // Minimum code point represented by a surrogate pair is 0x10000, 17 bits, + // four UTF-8 bytes + final char low; + if (i + 1 == in.length() + || !Character.isSurrogatePair(c, (low = in.charAt(++i)))) { + throw new UnpairedSurrogateException((i - 1), utf16Length); + } + int codePoint = Character.toCodePoint(c, low); + out[j++] = (byte) ((0xF << 4) | (codePoint >>> 18)); + out[j++] = (byte) (0x80 | (0x3F & (codePoint >>> 12))); + out[j++] = (byte) (0x80 | (0x3F & (codePoint >>> 6))); + out[j++] = (byte) (0x80 | (0x3F & codePoint)); + } else { + // If we are surrogates and we're not a surrogate pair, always throw an + // UnpairedSurrogateException instead of an ArrayOutOfBoundsException. + if ((Character.MIN_SURROGATE <= c && c <= Character.MAX_SURROGATE) + && (i + 1 == in.length() + || !Character.isSurrogatePair(c, in.charAt(i + 1)))) { + throw new UnpairedSurrogateException(i, utf16Length); + } + throw new ArrayIndexOutOfBoundsException("Failed writing " + c + " at index " + j); + } + } + return j; + } + + /** + * Encodes the given characters to the target {@link ByteBuffer} using UTF-8 encoding. + * + *

Selects an optimal algorithm based on the type of {@link ByteBuffer} (i.e. heap or direct) + * and the capabilities of the platform. + * + * @param in the source string to be encoded + * @param out the target buffer to receive the encoded string. + */ + @Override + public void encodeUtf8(CharSequence in, ByteBuffer out) { + if (out.hasArray()) { + int start = out.arrayOffset(); + int end = encodeUtf8Array(in, out.array(), start + out.position(), + out.remaining()); + out.position(end - start); + } else { + encodeUtf8Buffer(in, out); + } + } + + // These UTF-8 handling methods are copied from Guava's Utf8Unsafe class with + // a modification to throw a local exception. This exception can be caught + // to fallback to more lenient behavior. + static class UnpairedSurrogateException extends IllegalArgumentException { + UnpairedSurrogateException(int index, int length) { + super("Unpaired surrogate at index " + index + " of " + length); + } + } +} diff --git a/sceneformsrc/sceneform/src/main/res/drawable/sceneform_plane.png b/sceneformsrc/sceneform/src/main/res/drawable/sceneform_plane.png new file mode 100644 index 0000000..179e8a7 Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/drawable/sceneform_plane.png differ diff --git a/sceneformsrc/sceneform/src/main/res/raw/sceneform_camera_material.matc b/sceneformsrc/sceneform/src/main/res/raw/sceneform_camera_material.matc new file mode 100644 index 0000000..cbec83b Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/raw/sceneform_camera_material.matc differ diff --git a/sceneformsrc/sceneform/src/main/res/raw/sceneform_default_light_probe.sfb b/sceneformsrc/sceneform/src/main/res/raw/sceneform_default_light_probe.sfb new file mode 100644 index 0000000..27c29ee Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/raw/sceneform_default_light_probe.sfb differ diff --git a/sceneformsrc/sceneform/src/main/res/raw/sceneform_opaque_colored_material.matc b/sceneformsrc/sceneform/src/main/res/raw/sceneform_opaque_colored_material.matc new file mode 100644 index 0000000..909b8d9 Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/raw/sceneform_opaque_colored_material.matc differ diff --git a/sceneformsrc/sceneform/src/main/res/raw/sceneform_opaque_textured_material.matc b/sceneformsrc/sceneform/src/main/res/raw/sceneform_opaque_textured_material.matc new file mode 100644 index 0000000..2183504 Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/raw/sceneform_opaque_textured_material.matc differ diff --git a/sceneformsrc/sceneform/src/main/res/raw/sceneform_plane_material.matc b/sceneformsrc/sceneform/src/main/res/raw/sceneform_plane_material.matc new file mode 100644 index 0000000..2b3dcb9 Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/raw/sceneform_plane_material.matc differ diff --git a/sceneformsrc/sceneform/src/main/res/raw/sceneform_plane_shadow_material.matc b/sceneformsrc/sceneform/src/main/res/raw/sceneform_plane_shadow_material.matc new file mode 100644 index 0000000..e5c9e52 Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/raw/sceneform_plane_shadow_material.matc differ diff --git a/sceneformsrc/sceneform/src/main/res/raw/sceneform_transparent_colored_material.matc b/sceneformsrc/sceneform/src/main/res/raw/sceneform_transparent_colored_material.matc new file mode 100644 index 0000000..8183c77 Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/raw/sceneform_transparent_colored_material.matc differ diff --git a/sceneformsrc/sceneform/src/main/res/raw/sceneform_transparent_textured_material.matc b/sceneformsrc/sceneform/src/main/res/raw/sceneform_transparent_textured_material.matc new file mode 100644 index 0000000..bcf1ec4 Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/raw/sceneform_transparent_textured_material.matc differ diff --git a/sceneformsrc/sceneform/src/main/res/raw/sceneform_view_material.matc b/sceneformsrc/sceneform/src/main/res/raw/sceneform_view_material.matc new file mode 100755 index 0000000..028d3ae Binary files /dev/null and b/sceneformsrc/sceneform/src/main/res/raw/sceneform_view_material.matc differ diff --git a/sceneformsrc/settings.gradle b/sceneformsrc/settings.gradle new file mode 100644 index 0000000..94629eb --- /dev/null +++ b/sceneformsrc/settings.gradle @@ -0,0 +1 @@ +include ':sceneform' \ No newline at end of file diff --git a/sceneformux/.gitignore b/sceneformux/.gitignore new file mode 100644 index 0000000..863f89c --- /dev/null +++ b/sceneformux/.gitignore @@ -0,0 +1,13 @@ +# Android Studio configuration. +*.iml +.idea/ +# +# # Gradle configuration. +.gradle/ +build/ +# +# # User configuration. +local.properties +# +# # OS configurations. +.DS_Store diff --git a/sceneformux/build.gradle b/sceneformux/build.gradle new file mode 100644 index 0000000..4639c90 --- /dev/null +++ b/sceneformux/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/sceneformux/gradle.properties b/sceneformux/gradle.properties new file mode 100644 index 0000000..f82e516 --- /dev/null +++ b/sceneformux/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/sceneformux/gradle/wrapper/gradle-wrapper.jar b/sceneformux/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..87b738c Binary files /dev/null and b/sceneformux/gradle/wrapper/gradle-wrapper.jar differ diff --git a/sceneformux/gradle/wrapper/gradle-wrapper.properties b/sceneformux/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..413c3f1 --- /dev/null +++ b/sceneformux/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 05 19:37:35 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/sceneformux/gradlew b/sceneformux/gradlew new file mode 100755 index 0000000..af6708f --- /dev/null +++ b/sceneformux/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/sceneformux/gradlew.bat b/sceneformux/gradlew.bat new file mode 100644 index 0000000..6d57edc --- /dev/null +++ b/sceneformux/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/sceneformux/settings.gradle b/sceneformux/settings.gradle new file mode 100644 index 0000000..f28e1a3 --- /dev/null +++ b/sceneformux/settings.gradle @@ -0,0 +1,4 @@ +include ':ux' + +include ':sceneform' +project(':sceneform').projectDir=new File('../sceneformsrc/sceneform') diff --git a/sceneformux/ux/build.gradle b/sceneformux/ux/build.gradle new file mode 100644 index 0000000..0e8a452 --- /dev/null +++ b/sceneformux/ux/build.gradle @@ -0,0 +1,51 @@ +/* + * 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 { + // Use the Sceneform SDK built from the source files included in the sceneformsrc folder. + api project(":sceneform") + + implementation 'androidx.appcompat:appcompat:1.1.0' +} + +task compileUxAssets { +} + +preBuild.dependsOn compileUxAssets diff --git a/sceneformux/ux/sampledata/sceneform_face_mesh.obj b/sceneformux/ux/sampledata/sceneform_face_mesh.obj new file mode 100644 index 0000000..90b04a5 --- /dev/null +++ b/sceneformux/ux/sampledata/sceneform_face_mesh.obj @@ -0,0 +1,14 @@ +# Blender v2.78 (sub 0) OBJ File: '' +# www.blender.org +o Plane +v -0.500000 0.000000 0.000000 +v 0.500000 0.000000 0.000000 +v -0.500000 1.000000 0.000000 +v 0.500000 1.000000 0.000000 +vt 0.0000 0.0000 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 0.0000 1.0000 +vn 0.0000 0.0000 1.0000 +s off +f 1/1/1 2/2/1 4/3/1 3/4/1 diff --git a/sceneformux/ux/sampledata/sceneform_face_mesh_material.mat b/sceneformux/ux/sampledata/sceneform_face_mesh_material.mat new file mode 100644 index 0000000..b186a9c --- /dev/null +++ b/sceneformux/ux/sampledata/sceneform_face_mesh_material.mat @@ -0,0 +1,22 @@ +material { + "name" : "Face Mesh", + + "parameters" : [ + { + "type" : "sampler2d", + "name" : "texture" + } + ], + "requires" : [ + "position", + "uv0" + ], + "shadingModel" : "unlit", + "blending" : "transparent", +} +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = texture(materialParams_texture, getUV0()); + } +} diff --git a/sceneformux/ux/sampledata/sceneform_face_mesh_occluder.obj b/sceneformux/ux/sampledata/sceneform_face_mesh_occluder.obj new file mode 100644 index 0000000..90b04a5 --- /dev/null +++ b/sceneformux/ux/sampledata/sceneform_face_mesh_occluder.obj @@ -0,0 +1,14 @@ +# Blender v2.78 (sub 0) OBJ File: '' +# www.blender.org +o Plane +v -0.500000 0.000000 0.000000 +v 0.500000 0.000000 0.000000 +v -0.500000 1.000000 0.000000 +v 0.500000 1.000000 0.000000 +vt 0.0000 0.0000 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 0.0000 1.0000 +vn 0.0000 0.0000 1.0000 +s off +f 1/1/1 2/2/1 4/3/1 3/4/1 diff --git a/sceneformux/ux/sampledata/sceneform_face_mesh_occluder_material.mat b/sceneformux/ux/sampledata/sceneform_face_mesh_occluder_material.mat new file mode 100644 index 0000000..77d3b46 --- /dev/null +++ b/sceneformux/ux/sampledata/sceneform_face_mesh_occluder_material.mat @@ -0,0 +1,14 @@ +material { + "name" : "Face Mesh Occluder", + + "parameters" : [ + { + "type" : "float", + "name" : "unused" + } + ], + + "shadingModel" : "unlit", + "colorWrite" : false, + "depthWrite" : true, +} diff --git a/sceneformux/ux/src/main/AndroidManifest.xml b/sceneformux/ux/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3aad7b8 --- /dev/null +++ b/sceneformux/ux/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/ArFragment.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/ArFragment.java new file mode 100644 index 0000000..219f94d --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/ArFragment.java @@ -0,0 +1,77 @@ +/* + * Copyright 2018 Google LLC. All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.util.Log; +import android.widget.Toast; +import com.google.ar.core.Config; +import com.google.ar.core.Session; + +import com.google.ar.core.exceptions.UnavailableApkTooOldException; +import com.google.ar.core.exceptions.UnavailableArcoreNotInstalledException; +import com.google.ar.core.exceptions.UnavailableDeviceNotCompatibleException; +import com.google.ar.core.exceptions.UnavailableException; +import com.google.ar.core.exceptions.UnavailableSdkTooOldException; +import java.util.Collections; +import java.util.Set; + +/** + * Implements AR Required ArFragment. Does not require additional permissions and uses the default + * configuration for ARCore. + */ +public class ArFragment extends BaseArFragment { + private static final String TAG = "StandardArFragment"; + + @Override + public boolean isArRequired() { + return true; + } + + @Override + public String[] getAdditionalPermissions() { + return new String[0]; + } + + @Override + protected void handleSessionException(UnavailableException sessionException) { + + String message; + if (sessionException instanceof UnavailableArcoreNotInstalledException) { + message = "Please install ARCore"; + } else if (sessionException instanceof UnavailableApkTooOldException) { + message = "Please update ARCore"; + } else if (sessionException instanceof UnavailableSdkTooOldException) { + message = "Please update this app"; + } else if (sessionException instanceof UnavailableDeviceNotCompatibleException) { + message = "This device does not support AR"; + } else { + message = "Failed to create AR session"; + } + Log.e(TAG, "Error: " + message, sessionException); + Toast.makeText(requireActivity(), message, Toast.LENGTH_LONG).show(); + } + + @Override + protected Config getSessionConfiguration(Session session) { + return new Config(session); + } + + + @Override + protected Set getSessionFeatures() { + return Collections.emptySet(); + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseArFragment.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseArFragment.java new file mode 100644 index 0000000..b5266a8 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseArFragment.java @@ -0,0 +1,589 @@ +/* + * Copyright 2018 Google LLC. All Rights Reserved. + * + * 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 + */ +package com.google.ar.sceneform.ux; + +import android.Manifest; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.provider.Settings; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnWindowFocusChangeListener; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import com.google.ar.core.ArCoreApk; +import com.google.ar.core.Config; +import com.google.ar.core.Frame; +import com.google.ar.core.HitResult; +import com.google.ar.core.Plane; +import com.google.ar.core.Session; +import com.google.ar.core.Trackable; +import com.google.ar.core.TrackingState; + +import com.google.ar.core.exceptions.CameraNotAvailableException; +import com.google.ar.core.exceptions.UnavailableApkTooOldException; +import com.google.ar.core.exceptions.UnavailableArcoreNotInstalledException; +import com.google.ar.core.exceptions.UnavailableDeviceNotCompatibleException; +import com.google.ar.core.exceptions.UnavailableException; +import com.google.ar.core.exceptions.UnavailableSdkTooOldException; +import com.google.ar.sceneform.ArSceneView; +import com.google.ar.sceneform.FrameTime; +import com.google.ar.sceneform.HitTestResult; +import com.google.ar.sceneform.Scene; +import com.google.ar.sceneform.rendering.ModelRenderable; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** The AR fragment brings in the required view layout and controllers for common AR features. */ +public abstract class BaseArFragment extends Fragment + implements Scene.OnPeekTouchListener, Scene.OnUpdateListener { + private static final String TAG = BaseArFragment.class.getSimpleName(); + + /** Invoked when the ARCore Session is initialized. */ + public interface OnSessionInitializationListener { + /** + * The callback will only be invoked once after a Session is initialized and before it is + * resumed for the first time. + * + * @see #setOnSessionInitializationListener(OnTapArPlaneListener) + * @param session The ARCore Session. + */ + void onSessionInitialization(Session session); + } + + /** Invoked when an ARCore plane is tapped. */ + public interface OnTapArPlaneListener { + /** + * Called when an ARCore plane is tapped. The callback will only be invoked if no {@link + * com.google.ar.sceneform.Node} was tapped. + * + * @see #setOnTapArPlaneListener(OnTapArPlaneListener) + * @param hitResult The ARCore hit result that occurred when tapping the plane + * @param plane The ARCore Plane that was tapped + * @param motionEvent the motion event that triggered the tap + */ + void onTapPlane(HitResult hitResult, Plane plane, MotionEvent motionEvent); + } + + private static final int RC_PERMISSIONS = 1010; + private boolean installRequested; + private boolean sessionInitializationFailed = false; + private ArSceneView arSceneView; + private PlaneDiscoveryController planeDiscoveryController; + private TransformationSystem transformationSystem; + private GestureDetector gestureDetector; + private FrameLayout frameLayout; + private boolean isStarted; + private boolean canRequestDangerousPermissions = true; + @Nullable private OnSessionInitializationListener onSessionInitializationListener; + @Nullable private OnTapArPlaneListener onTapArPlaneListener; + + @SuppressWarnings({"initialization"}) + private final OnWindowFocusChangeListener onFocusListener = + (hasFocus -> onWindowFocusChanged(hasFocus)); + + /** Gets the ArSceneView for this fragment. */ + public ArSceneView getArSceneView() { + return arSceneView; + } + + /** + * Gets the plane discovery controller, which displays instructions for how to scan for planes. + */ + public PlaneDiscoveryController getPlaneDiscoveryController() { + return planeDiscoveryController; + } + + /** + * Gets the transformation system, which is used by {@link TransformableNode} for detecting + * gestures and coordinating which node is selected. + */ + public TransformationSystem getTransformationSystem() { + return transformationSystem; + } + + /** + * Registers a callback to be invoked when the ARCore Session is initialized. The callback will + * only be invoked once after the Session is initialized and before it is resumed. + * + * @param onSessionInitializationListener the {@link OnSessionInitializationListener} to attach. + */ + public void setOnSessionInitializationListener( + @Nullable OnSessionInitializationListener onSessionInitializationListener) { + this.onSessionInitializationListener = onSessionInitializationListener; + } + + /** + * Registers a callback to be invoked when an ARCore Plane is tapped. The callback will only be + * invoked if no {@link com.google.ar.sceneform.Node} was tapped. + * + * @param onTapArPlaneListener the {@link OnTapArPlaneListener} to attach + */ + public void setOnTapArPlaneListener(@Nullable OnTapArPlaneListener onTapArPlaneListener) { + this.onTapArPlaneListener = onTapArPlaneListener; + } + + @Override + @SuppressWarnings({"initialization"}) + // Suppress @UnderInitialization warning. + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + frameLayout = + (FrameLayout) inflater.inflate(R.layout.sceneform_ux_fragment_layout, container, false); + arSceneView = (ArSceneView) frameLayout.findViewById(R.id.sceneform_ar_scene_view); + + // Setup the instructions view. + View instructionsView = loadPlaneDiscoveryView(inflater, container); + if (instructionsView != null) { + frameLayout.addView(instructionsView); + } + planeDiscoveryController = new PlaneDiscoveryController(instructionsView); + + if (Build.VERSION.SDK_INT < VERSION_CODES.N) { + // Enforce API level 24 + return frameLayout; + } + + transformationSystem = makeTransformationSystem(); + + gestureDetector = + new GestureDetector( + getContext(), + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + onSingleTap(e); + return true; + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + }); + + arSceneView.getScene().addOnPeekTouchListener(this); + arSceneView.getScene().addOnUpdateListener(this); + + if (isArRequired()) { + // Request permissions + requestDangerousPermissions(); + } + + // Make the app immersive and don't turn off the display. + arSceneView.getViewTreeObserver().addOnWindowFocusChangeListener(onFocusListener); + return frameLayout; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + arSceneView.getViewTreeObserver().removeOnWindowFocusChangeListener(onFocusListener); + } + + /** + * Returns true if this application is AR Required, false if AR Optional. This is called when + * initializing the application and the session. + */ + public abstract boolean isArRequired(); + + /** + * Returns an array of dangerous permissions that are required by the app in addition to + * Manifest.permission.CAMERA, which is needed by ARCore. If no additional permissions are needed, + * an empty array should be returned. + */ + public abstract String[] getAdditionalPermissions(); + + /** + * Starts the process of requesting dangerous permissions. This combines the CAMERA permission + * required of ARCore and any permissions returned from getAdditionalPermissions(). There is no + * specific processing on the result of the request, subclasses can override + * onRequestPermissionsResult() if additional processing is needed. + * + *

{@link #setCanRequestDangerousPermissions(Boolean)} can stop this function from doing + * anything. + */ + protected void requestDangerousPermissions() { + if (!canRequestDangerousPermissions) { + // If this is in progress, don't do it again. + return; + } + canRequestDangerousPermissions = false; + + List permissions = new ArrayList(); + String[] additionalPermissions = getAdditionalPermissions(); + int permissionLength = additionalPermissions != null ? additionalPermissions.length : 0; + for (int i = 0; i < permissionLength; ++i) { + if (ActivityCompat.checkSelfPermission(requireActivity(), additionalPermissions[i]) + != PackageManager.PERMISSION_GRANTED) { + permissions.add(additionalPermissions[i]); + } + } + + // Always check for camera permission + if (ActivityCompat.checkSelfPermission(requireActivity(), Manifest.permission.CAMERA) + != PackageManager.PERMISSION_GRANTED) { + permissions.add(Manifest.permission.CAMERA); + } + + if (!permissions.isEmpty()) { + // Request the permissions + requestPermissions(permissions.toArray(new String[permissions.size()]), RC_PERMISSIONS); + } + } + + /** + * Receives the results for permission requests. + * + *

Brings up a dialog to request permissions. The dialog can send the user to the Settings app, + * or finish the activity. + */ + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { + if (ActivityCompat.checkSelfPermission(requireActivity(), Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + return; + } + AlertDialog.Builder builder; + builder = + new AlertDialog.Builder(requireActivity(), android.R.style.Theme_Material_Dialog_Alert); + + builder + .setTitle("Camera permission required") + .setMessage("Add camera permission via Settings?") + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // If Ok was hit, bring up the Settings app. + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", requireActivity().getPackageName(), null)); + requireActivity().startActivity(intent); + // When the user closes the Settings app, allow the app to resume. + // Allow the app to ask for permissions again now. + setCanRequestDangerousPermissions(true); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .setOnDismissListener( + new OnDismissListener() { + @Override + public void onDismiss(final DialogInterface arg0) { + // canRequestDangerousPermissions will be true if "OK" was selected from the dialog, + // false otherwise. If "OK" was selected do nothing on dismiss, the app will + // continue and may ask for permission again if needed. + // If anything else happened, finish the activity when this dialog is + // dismissed. + if (!getCanRequestDangerousPermissions()) { + requireActivity().finish(); + } + } + }) + .show(); + } + + /** + * If true, {@link #requestDangerousPermissions()} returns without doing anything, if false + * permissions will be requested + */ + protected Boolean getCanRequestDangerousPermissions() { + return canRequestDangerousPermissions; + } + + /** + * If true, {@link #requestDangerousPermissions()} returns without doing anything, if false + * permissions will be requested + */ + protected void setCanRequestDangerousPermissions(Boolean canRequestDangerousPermissions) { + this.canRequestDangerousPermissions = canRequestDangerousPermissions; + } + + @Override + public void onResume() { + super.onResume(); + if (isArRequired() && arSceneView.getSession() == null) { + initializeSession(); + } + start(); + } + + + protected final boolean requestInstall() throws UnavailableException { + switch (ArCoreApk.getInstance().requestInstall(requireActivity(), !installRequested)) { + case INSTALL_REQUESTED: + installRequested = true; + return true; + case INSTALLED: + break; + } + return false; + } + + /** + * Initializes the ARCore session. The CAMERA permission is checked before checking the + * installation state of ARCore. Once the permissions and installation are OK, the method + * #getSessionConfiguration(Session session) is called to get the session configuration to use. + * Sceneform requires that the ARCore session be updated using LATEST_CAMERA_IMAGE to avoid + * blocking while drawing. This mode is set on the configuration object returned from the + * subclass. + */ + protected final void initializeSession() { + + // Only try once + if (sessionInitializationFailed) { + return; + } + // if we have the camera permission, create the session + if (ContextCompat.checkSelfPermission(requireActivity(), "android.permission.CAMERA") + == PackageManager.PERMISSION_GRANTED) { + + UnavailableException sessionException = null; + try { + if (requestInstall()) { + return; + } + + Session session = createSession(); + + if (this.onSessionInitializationListener != null) { + this.onSessionInitializationListener.onSessionInitialization(session); + } + + Config config = getSessionConfiguration(session); + // Force the non-blocking mode for the session. + + config.setUpdateMode(Config.UpdateMode.LATEST_CAMERA_IMAGE); + session.configure(config); + getArSceneView().setupSession(session); + return; + } catch (UnavailableException e) { + sessionException = e; + } catch (Exception e) { + sessionException = new UnavailableException(); + sessionException.initCause(e); + } + sessionInitializationFailed = true; + handleSessionException(sessionException); + + } else { + requestDangerousPermissions(); + } + } + + private Session createSession() + throws UnavailableSdkTooOldException, UnavailableDeviceNotCompatibleException, + UnavailableArcoreNotInstalledException, UnavailableApkTooOldException { + Session session = createSessionWithFeatures(); + if (session == null) { + session = new Session(requireActivity()); + } + return session; + } + + /** + * Creates the ARCore Session with the with features defined in #getSessionFeatures. If this + * returns null, the Session will be created with the default features. + */ + + protected @Nullable Session createSessionWithFeatures() + throws UnavailableSdkTooOldException, UnavailableDeviceNotCompatibleException, + UnavailableArcoreNotInstalledException, UnavailableApkTooOldException { + return new Session(requireActivity(), getSessionFeatures()); + } + + /** + * Creates the transformation system used by this fragment. Can be overridden to create a custom + * transformation system. + */ + protected TransformationSystem makeTransformationSystem() { + FootprintSelectionVisualizer selectionVisualizer = new FootprintSelectionVisualizer(); + + TransformationSystem transformationSystem = + new TransformationSystem(getResources().getDisplayMetrics(), selectionVisualizer); + + setupSelectionRenderable(selectionVisualizer); + + return transformationSystem; + } + + @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"}) + + protected void setupSelectionRenderable(FootprintSelectionVisualizer selectionVisualizer) { + ModelRenderable.builder() + .setSource(getActivity(), R.raw.sceneform_footprint) + .setIsFilamentGltf(true) + .build() + .thenAccept( + renderable -> { + // If the selection visualizer already has a footprint renderable, then it was set to + // something custom. Don't override the custom visual. + if (selectionVisualizer.getFootprintRenderable() == null) { + selectionVisualizer.setFootprintRenderable(renderable); + } + }) + .exceptionally( + throwable -> { + Toast toast = + Toast.makeText( + getContext(), "Unable to load footprint renderable", Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER, 0, 0); + toast.show(); + return null; + }); + } + + protected abstract void handleSessionException(UnavailableException sessionException); + + protected abstract Config getSessionConfiguration(Session session); + + /** + * Specifies additional features for creating an ARCore {@link com.google.ar.core.Session}. See + * {@link com.google.ar.core.Session.Feature}. + */ + + protected abstract Set getSessionFeatures(); + + protected void onWindowFocusChanged(boolean hasFocus) { + FragmentActivity activity = getActivity(); + if (hasFocus && activity != null) { + // Standard Android full-screen functionality. + activity + .getWindow() + .getDecorView() + .setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + @Override + public void onPause() { + super.onPause(); + stop(); + } + + @Override + public void onDestroy() { + stop(); + arSceneView.destroy(); + super.onDestroy(); + } + + @Override + public void onPeekTouch(HitTestResult hitTestResult, MotionEvent motionEvent) { + transformationSystem.onTouch(hitTestResult, motionEvent); + + if (hitTestResult.getNode() == null) { + gestureDetector.onTouchEvent(motionEvent); + } + } + + @Override + public void onUpdate(FrameTime frameTime) { + Frame frame = arSceneView.getArFrame(); + if (frame == null) { + return; + } + + for (Plane plane : frame.getUpdatedTrackables(Plane.class)) { + if (plane.getTrackingState() == TrackingState.TRACKING) { + planeDiscoveryController.hide(); + } + } + } + + private void start() { + if (isStarted) { + return; + } + + if (getActivity() != null) { + isStarted = true; + try { + arSceneView.resume(); + } catch (CameraNotAvailableException ex) { + sessionInitializationFailed = true; + } + if (!sessionInitializationFailed) { + planeDiscoveryController.show(); + } + } + } + + private void stop() { + if (!isStarted) { + return; + } + + isStarted = false; + planeDiscoveryController.hide(); + arSceneView.pause(); + } + + // Load the default view we use for the plane discovery instructions. + @Nullable + + private View loadPlaneDiscoveryView(LayoutInflater inflater, @Nullable ViewGroup container) { + return inflater.inflate(R.layout.sceneform_plane_discovery_layout, container, false); + } + + private void onSingleTap(MotionEvent motionEvent) { + Frame frame = arSceneView.getArFrame(); + + transformationSystem.selectNode(null); + + // Local variable for nullness static-analysis. + OnTapArPlaneListener onTapArPlaneListener = this.onTapArPlaneListener; + + if (frame != null && onTapArPlaneListener != null) { + if (motionEvent != null && frame.getCamera().getTrackingState() == TrackingState.TRACKING) { + for (HitResult hit : frame.hitTest(motionEvent)) { + Trackable trackable = hit.getTrackable(); + if (trackable instanceof Plane && ((Plane) trackable).isPoseInPolygon(hit.getHitPose())) { + Plane plane = (Plane) trackable; + onTapArPlaneListener.onTapPlane(hit, plane, motionEvent); + break; + } + } + } + } + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseGesture.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseGesture.java new file mode 100644 index 0000000..d2ea3bc --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseGesture.java @@ -0,0 +1,143 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.view.MotionEvent; +import androidx.annotation.Nullable; +import com.google.ar.sceneform.HitTestResult; +import com.google.ar.sceneform.Node; + +/** + * Base class for a gesture. + * + *

A gesture represents a sequence of touch events that are detected to represent a particular + * type of motion (i.e. Dragging, Pinching). + * + *

Gestures are created and updated by BaseGestureRecognizer's. + */ +public abstract class BaseGesture> { + /** Interface definition for callbacks to be invoked by a {@link BaseGesture}. */ + public interface OnGestureEventListener> { + void onUpdated(T gesture); + + void onFinished(T gesture); + } + + protected final GesturePointersUtility gesturePointersUtility; + + private boolean hasStarted; + private boolean justStarted; + private boolean hasFinished; + private boolean wasCancelled; + + @Nullable protected Node targetNode; + @Nullable private OnGestureEventListener eventListener; + + public BaseGesture(GesturePointersUtility gesturePointersUtility) { + this.gesturePointersUtility = gesturePointersUtility; + } + + public boolean hasStarted() { + return hasStarted; + } + + public boolean justStarted() { + return justStarted; + } + + public boolean hasFinished() { + return hasFinished; + } + + public boolean wasCancelled() { + return wasCancelled; + } + + @Nullable + public Node getTargetNode() { + return targetNode; + } + + public float inchesToPixels(float inches) { + return gesturePointersUtility.inchesToPixels(inches); + } + + public float pixelsToInches(float pixels) { + return gesturePointersUtility.pixelsToInches(pixels); + } + + public void setGestureEventListener(@Nullable OnGestureEventListener listener) { + eventListener = listener; + } + + public void onTouch(HitTestResult hitTestResult, MotionEvent motionEvent) { + if (!hasStarted && canStart(hitTestResult, motionEvent)) { + start(hitTestResult, motionEvent); + return; + } + justStarted = false; + if (hasStarted) { + if (updateGesture(hitTestResult, motionEvent)) { + dispatchUpdateEvent(); + } + } + } + + protected abstract boolean canStart(HitTestResult hitTestResult, MotionEvent motionEvent); + + protected abstract void onStart(HitTestResult hitTestResult, MotionEvent motionEvent); + + protected abstract boolean updateGesture(HitTestResult hitTestResult, MotionEvent motionEvent); + + protected abstract void onCancel(); + + protected abstract void onFinish(); + + protected void cancel() { + wasCancelled = true; + onCancel(); + complete(); + } + + protected void complete() { + hasFinished = true; + if (hasStarted) { + onFinish(); + dispatchFinishedEvent(); + } + } + + private void start(HitTestResult hitTestResult, MotionEvent motionEvent) { + hasStarted = true; + justStarted = true; + onStart(hitTestResult, motionEvent); + } + + private void dispatchUpdateEvent() { + if (eventListener != null) { + eventListener.onUpdated(getSelf()); + } + } + + private void dispatchFinishedEvent() { + if (eventListener != null) { + eventListener.onFinished(getSelf()); + } + } + + // For compile-time safety so we don't need to cast when dispatching events. + protected abstract T getSelf(); +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseGestureRecognizer.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseGestureRecognizer.java new file mode 100644 index 0000000..41c8b4b --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseGestureRecognizer.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.view.MotionEvent; +import com.google.ar.sceneform.HitTestResult; +import java.util.ArrayList; + +/** + * Base class for all Gesture Recognizers (i.e. DragGestureRecognizer). + * + *

A Gesture recognizer processes touch input to determine if a gesture should start and fires an + * event when the gesture is started. + * + *

To determine when an gesture is finished/updated, listen to the events on the gesture object. + */ +public abstract class BaseGestureRecognizer> { + /** Interface definition for a callbacks to be invoked when a {@link BaseGesture} starts. */ + public interface OnGestureStartedListener> { + void onGestureStarted(T gesture); + } + + protected final GesturePointersUtility gesturePointersUtility; + protected final ArrayList gestures = new ArrayList<>(); + + private final ArrayList> gestureStartedListeners; + + @SuppressWarnings("initialization") // Suppress @UnderInitialization warning. + public BaseGestureRecognizer(GesturePointersUtility gesturePointersUtility) { + this.gesturePointersUtility = gesturePointersUtility; + gestureStartedListeners = new ArrayList<>(); + } + + public void addOnGestureStartedListener(OnGestureStartedListener listener) { + if (!gestureStartedListeners.contains(listener)) { + gestureStartedListeners.add(listener); + } + } + + public void removeOnGestureStartedListener(OnGestureStartedListener listener) { + gestureStartedListeners.remove(listener); + } + + public void onTouch(HitTestResult hitTestResult, MotionEvent motionEvent) { + // Instantiate gestures based on touch input. + // Just because a gesture was created, doesn't mean that it is started. + // For example, a DragGesture is created when the user touch's down, + // but doesn't actually start until the touch has moved beyond a threshold. + tryCreateGestures(hitTestResult, motionEvent); + + // Propagate event to gestures and determine if they should start. + for (int i = 0; i < gestures.size(); i++) { + T gesture = gestures.get(i); + gesture.onTouch(hitTestResult, motionEvent); + + if (gesture.justStarted()) { + dispatchGestureStarted(gesture); + } + } + + removeFinishedGestures(); + } + + protected abstract void tryCreateGestures(HitTestResult hitTestResult, MotionEvent motionEvent); + + private void dispatchGestureStarted(T gesture) { + for (int i = 0; i < gestureStartedListeners.size(); i++) { + OnGestureStartedListener listener = gestureStartedListeners.get(i); + listener.onGestureStarted(gesture); + } + } + + private void removeFinishedGestures() { + for (int i = gestures.size() - 1; i >= 0; i--) { + T gesture = gestures.get(i); + if (gesture.hasFinished()) { + gestures.remove(i); + } + } + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseTransformableNode.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseTransformableNode.java new file mode 100644 index 0000000..33e34f3 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseTransformableNode.java @@ -0,0 +1,82 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.view.MotionEvent; +import com.google.ar.sceneform.HitTestResult; +import com.google.ar.sceneform.Node; +import java.util.ArrayList; + +/** + * Base class for nodes that can be transformed using gestures from {@link TransformationSystem}. + */ +public abstract class BaseTransformableNode extends Node implements Node.OnTapListener { + private final TransformationSystem transformationSystem; + private final ArrayList> controllers = new ArrayList<>(); + + @SuppressWarnings("initialization") + public BaseTransformableNode(TransformationSystem transformationSystem) { + this.transformationSystem = transformationSystem; + + setOnTapListener(this); + } + + public TransformationSystem getTransformationSystem() { + return transformationSystem; + } + + /** Returns true if any of the transformation controllers are actively transforming this node. */ + public boolean isTransforming() { + for (int i = 0; i < controllers.size(); i++) { + if (controllers.get(i).isTransforming()) { + return true; + } + } + + return false; + } + + /** Returns true if this node is currently selected by the TransformationSystem. */ + public boolean isSelected() { + return transformationSystem.getSelectedNode() == this; + } + + /** + * Sets this as the selected node in the TransformationSystem if there is no currently selected + * node or if the currently selected node is not actively being transformed. + * + * @see TransformableNode#isTransforming + * @return true if the node was successfully selected + */ + public boolean select() { + return transformationSystem.selectNode(this); + } + + @Override + public void onTap(HitTestResult hitTestResult, MotionEvent motionEvent) { + select(); + } + + protected void addTransformationController( + BaseTransformationController transformationController) { + controllers.add(transformationController); + } + + protected void removeTransformationController( + BaseTransformationController transformationController) { + controllers.remove(transformationController); + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseTransformationController.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseTransformationController.java new file mode 100644 index 0000000..9e46213 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/BaseTransformationController.java @@ -0,0 +1,164 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import com.google.ar.sceneform.FrameTime; +import com.google.ar.sceneform.Node; + +/** + * Manipulates the transform properties (i.e. scale/rotation/translation) of a {@link + * BaseTransformableNode} by responding to Gestures via a {@link BaseGestureRecognizer}. + * + *

Example's include, changing the {@link TransformableNode}'s Scale based on a Pinch Gesture. + */ +public abstract class BaseTransformationController> + implements BaseGestureRecognizer.OnGestureStartedListener, + BaseGesture.OnGestureEventListener, + Node.LifecycleListener { + private final BaseTransformableNode transformableNode; + private final BaseGestureRecognizer gestureRecognizer; + + @Nullable private T activeGesture; + private boolean enabled; + private boolean activeAndEnabled; + + @SuppressWarnings("initialization") // Suppress @UnderInitialization warning. + public BaseTransformationController( + BaseTransformableNode transformableNode, BaseGestureRecognizer gestureRecognizer) { + this.transformableNode = transformableNode; + this.transformableNode.addLifecycleListener(this); + this.gestureRecognizer = gestureRecognizer; + setEnabled(true); + } + + public boolean isEnabled() { + return enabled; + } + + @Nullable + public T getActiveGesture() { + return activeGesture; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + updateActiveAndEnabled(); + } + + public boolean isTransforming() { + return activeGesture != null; + } + + public BaseTransformableNode getTransformableNode() { + return transformableNode; + } + + // --------------------------------------------------------------------------------------- + // Implementation of interface Node.LifecycleListener + // --------------------------------------------------------------------------------------- + + @Override + @CallSuper + public void onActivated(Node node) { + updateActiveAndEnabled(); + } + + @Override + public void onUpdated(Node node, FrameTime frameTime) {} + + @Override + @CallSuper + public void onDeactivated(Node node) { + updateActiveAndEnabled(); + } + + // --------------------------------------------------------------------------------------- + // Implementation of interface BaseGestureRecognizer.OnGestureStartedListener + // --------------------------------------------------------------------------------------- + + @Override + public void onGestureStarted(T gesture) { + if (isTransforming()) { + return; + } + + if (canStartTransformation(gesture)) { + setActiveGesture(gesture); + } + } + + // --------------------------------------------------------------------------------------- + // Implementation of interface BaseGesture.OnGestureEventListener + // --------------------------------------------------------------------------------------- + + @SuppressWarnings("UngroupedOverloads") // This is not an overload, it is a different interface. + @Override + public void onUpdated(T gesture) { + onContinueTransformation(gesture); + } + + @Override + public void onFinished(T gesture) { + onEndTransformation(gesture); + setActiveGesture(null); + } + + protected abstract boolean canStartTransformation(T gesture); + + protected abstract void onContinueTransformation(T gesture); + + protected abstract void onEndTransformation(T gesture); + + private void setActiveGesture(@Nullable T gesture) { + if (activeGesture != null) { + activeGesture.setGestureEventListener(null); + } + + activeGesture = gesture; + + if (activeGesture != null) { + activeGesture.setGestureEventListener(this); + } + } + + private void updateActiveAndEnabled() { + boolean newActiveAndEnabled = getTransformableNode().isActive() && enabled; + if (newActiveAndEnabled == activeAndEnabled) { + return; + } + + activeAndEnabled = newActiveAndEnabled; + + if (activeAndEnabled) { + connectToRecognizer(); + } else { + disconnectFromRecognizer(); + if (activeGesture != null) { + activeGesture.cancel(); + } + } + } + + private void connectToRecognizer() { + gestureRecognizer.addOnGestureStartedListener(this); + } + + private void disconnectFromRecognizer() { + gestureRecognizer.removeOnGestureStartedListener(this); + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/DragGesture.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/DragGesture.java new file mode 100644 index 0000000..4037cc3 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/DragGesture.java @@ -0,0 +1,154 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.util.Log; +import android.view.MotionEvent; +import com.google.ar.sceneform.HitTestResult; +import com.google.ar.sceneform.math.Vector3; + +/** Gesture for when the user performs a drag motion on the touch screen. */ +public class DragGesture extends BaseGesture { + private static final String TAG = DragGesture.class.getSimpleName(); + + /** Interface definition for callbacks to be invoked by a {@link DragGesture}. */ + public interface OnGestureEventListener extends BaseGesture.OnGestureEventListener {} + + private final Vector3 startPosition; + private final Vector3 position; + private final Vector3 delta; + private final int pointerId; + + private static final float SLOP_INCHES = 0.1f; + private static final boolean DRAG_GESTURE_DEBUG = false; + + public DragGesture( + GesturePointersUtility gesturePointersUtility, + HitTestResult hitTestResult, + MotionEvent motionEvent) { + super(gesturePointersUtility); + + pointerId = motionEvent.getPointerId(motionEvent.getActionIndex()); + startPosition = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId); + position = new Vector3(startPosition); + delta = Vector3.zero(); + targetNode = hitTestResult.getNode(); + debugLog("Created: " + pointerId); + } + + public Vector3 getPosition() { + return new Vector3(position); + } + + public Vector3 getDelta() { + return new Vector3(delta); + } + + @Override + protected boolean canStart(HitTestResult hitTestResult, MotionEvent motionEvent) { + int actionId = motionEvent.getPointerId(motionEvent.getActionIndex()); + int action = motionEvent.getActionMasked(); + + if (gesturePointersUtility.isPointerIdRetained(pointerId)) { + cancel(); + return false; + } + + if (actionId == pointerId + && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP)) { + cancel(); + return false; + } else if (action == MotionEvent.ACTION_CANCEL) { + cancel(); + return false; + } + + if (action != MotionEvent.ACTION_MOVE) { + return false; + } + + if (motionEvent.getPointerCount() > 1) { + for (int i = 0; i < motionEvent.getPointerCount(); i++) { + int id = motionEvent.getPointerId(i); + if (id != pointerId && !gesturePointersUtility.isPointerIdRetained(id)) { + return false; + } + } + } + + Vector3 newPosition = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId); + float diff = Vector3.subtract(newPosition, startPosition).length(); + float slopPixels = gesturePointersUtility.inchesToPixels(SLOP_INCHES); + if (diff >= slopPixels) { + return true; + } + + return false; + } + + @Override + protected void onStart(HitTestResult hitTestResult, MotionEvent motionEvent) { + debugLog("Started: " + pointerId); + + position.set(GesturePointersUtility.motionEventToPosition(motionEvent, pointerId)); + gesturePointersUtility.retainPointerId(pointerId); + } + + @Override + protected boolean updateGesture(HitTestResult hitTestResult, MotionEvent motionEvent) { + int actionId = motionEvent.getPointerId(motionEvent.getActionIndex()); + int action = motionEvent.getActionMasked(); + + if (action == MotionEvent.ACTION_MOVE) { + Vector3 newPosition = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId); + if (!Vector3.equals(newPosition, position)) { + delta.set(Vector3.subtract(newPosition, position)); + position.set(newPosition); + debugLog("Updated: " + pointerId + " : " + position); + return true; + } + } else if (actionId == pointerId + && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP)) { + complete(); + } else if (action == MotionEvent.ACTION_CANCEL) { + cancel(); + } + + return false; + } + + @Override + protected void onCancel() { + debugLog("Cancelled: " + pointerId); + } + + @Override + protected void onFinish() { + debugLog("Finished: " + pointerId); + gesturePointersUtility.releasePointerId(pointerId); + } + + @Override + protected DragGesture getSelf() { + return this; + } + + private static void debugLog(String log) { + if (DRAG_GESTURE_DEBUG) { + Log.d(TAG, "DragGesture:[" + log + "]"); + } + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/DragGestureRecognizer.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/DragGestureRecognizer.java new file mode 100644 index 0000000..ad06dc0 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/DragGestureRecognizer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.view.MotionEvent; +import com.google.ar.sceneform.HitTestResult; + +/** Gesture Recognizer for when the user performs a drag motion on the touch screen. */ +public class DragGestureRecognizer extends BaseGestureRecognizer { + /** Interface definition for a callbacks to be invoked when a {@link DragGesture} starts. */ + public interface OnGestureStartedListener + extends BaseGestureRecognizer.OnGestureStartedListener {} + + public DragGestureRecognizer(GesturePointersUtility gesturePointersUtility) { + super(gesturePointersUtility); + } + + @Override + protected void tryCreateGestures(HitTestResult hitTestResult, MotionEvent motionEvent) { + int action = motionEvent.getActionMasked(); + int actionId = motionEvent.getPointerId(motionEvent.getActionIndex()); + boolean touchBegan = + action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN; + + if (touchBegan && !gesturePointersUtility.isPointerIdRetained(actionId)) { + gestures.add(new DragGesture(gesturePointersUtility, hitTestResult, motionEvent)); + } + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/FootprintSelectionVisualizer.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/FootprintSelectionVisualizer.java new file mode 100644 index 0000000..a7d7013 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/FootprintSelectionVisualizer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import androidx.annotation.Nullable; +import com.google.ar.sceneform.Node; +import com.google.ar.sceneform.rendering.ModelRenderable; + +/** + * Visualizes that a {@link BaseTransformableNode} is selected by rendering a footprint for the + * selected node. + */ +public class FootprintSelectionVisualizer implements SelectionVisualizer { + private final Node footprintNode; + @Nullable private ModelRenderable footprintRenderable; + + public FootprintSelectionVisualizer() { + footprintNode = new Node(); + } + + public void setFootprintRenderable(ModelRenderable renderable) { + ModelRenderable copyRenderable = renderable.makeCopy(); + footprintNode.setRenderable(copyRenderable); + copyRenderable.setCollisionShape(null); + footprintRenderable = copyRenderable; + } + + @Nullable + public ModelRenderable getFootprintRenderable() { + return footprintRenderable; + } + + @Override + public void applySelectionVisual(BaseTransformableNode node) { + footprintNode.setParent(node); + } + + @Override + public void removeSelectionVisual(BaseTransformableNode node) { + footprintNode.setParent(null); + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/GesturePointersUtility.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/GesturePointersUtility.java new file mode 100644 index 0000000..b464724 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/GesturePointersUtility.java @@ -0,0 +1,65 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.MotionEvent; +import com.google.ar.sceneform.math.Vector3; +import java.util.HashSet; + +/** + * Retains/Releases pointer Ids so that each pointer can only be used in one gesture at a time. + * Provides helper functions for converting touch coordinates between pixels and inches. + */ +public class GesturePointersUtility { + private final DisplayMetrics displayMetrics; + private final HashSet retainedPointerIds; + + public GesturePointersUtility(DisplayMetrics displayMetrics) { + this.displayMetrics = displayMetrics; + retainedPointerIds = new HashSet<>(); + } + + public void retainPointerId(int pointerId) { + if (!isPointerIdRetained(pointerId)) { + retainedPointerIds.add(pointerId); + } + } + + public void releasePointerId(int pointerId) { + retainedPointerIds.remove(Integer.valueOf(pointerId)); + } + + public boolean isPointerIdRetained(int pointerId) { + return retainedPointerIds.contains(pointerId); + } + + public float inchesToPixels(float inches) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_IN, inches, displayMetrics); + } + + public float pixelsToInches(float pixels) { + float inchOfPixels = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_IN, 1.0f, displayMetrics); + return pixels / inchOfPixels; + } + + public static Vector3 motionEventToPosition(MotionEvent me, int pointerId) { + int index = me.findPointerIndex(pointerId); + return new Vector3(me.getX(index), me.getY(index), 0.0f); + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/HandMotionAnimation.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/HandMotionAnimation.java new file mode 100644 index 0000000..830d58d --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/HandMotionAnimation.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 Google LLC. All Rights Reserved. + * + * 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 + */ +package com.google.ar.sceneform.ux; + +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.Transformation; + + +/** This drives the AR hand motion animation. */ + +public class HandMotionAnimation extends Animation { + private final View handImageView; + private final View containerView; + private static final float TWO_PI = (float) Math.PI * 2.0f; + private static final float HALF_PI = (float) Math.PI / 2.0f; + + public HandMotionAnimation(View containerView, View handImageView) { + this.handImageView = handImageView; + this.containerView = containerView; + + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation transformation) { + float startAngle = HALF_PI; + float progressAngle = TWO_PI * interpolatedTime; + float currentAngle = startAngle + progressAngle; + + float handWidth = handImageView.getWidth(); + float radius = handImageView.getResources().getDisplayMetrics().density * 25.0f; + + float xPos = radius * 2.0f * (float) Math.cos(currentAngle); + float yPos = radius * (float) Math.sin(currentAngle); + + xPos += containerView.getWidth() / 2.0f; + yPos += containerView.getHeight() / 2.0f; + + xPos -= handWidth / 2.0f; + yPos -= handImageView.getHeight() / 2.0f; + + // Position the hand. + handImageView.setX(xPos); + handImageView.setY(yPos); + + handImageView.invalidate(); + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/HandMotionView.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/HandMotionView.java new file mode 100644 index 0000000..1bfb625 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/HandMotionView.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Google LLC. All Rights Reserved. + * + * 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 + */ +package com.google.ar.sceneform.ux; + +import android.app.Activity; +import android.content.Context; +import android.util.AttributeSet; +import android.view.animation.Animation; +import android.widget.FrameLayout; +import androidx.appcompat.widget.AppCompatImageView; + + +/** This view contains the hand motion instructions with animation. */ + +public class HandMotionView extends AppCompatImageView { + private HandMotionAnimation animation; + private static final long ANIMATION_SPEED_MS = 2500; + + public HandMotionView(Context context) { + super(context); + } + + public HandMotionView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + clearAnimation(); + + FrameLayout container = + (FrameLayout) ((Activity) getContext()).findViewById(R.id.sceneform_hand_layout); + + animation = new HandMotionAnimation(container, this); + animation.setRepeatCount(Animation.INFINITE); + animation.setDuration(ANIMATION_SPEED_MS); + animation.setStartOffset(1000); + + startAnimation(animation); + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/PinchGesture.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/PinchGesture.java new file mode 100644 index 0000000..32d710f --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/PinchGesture.java @@ -0,0 +1,206 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.util.Log; +import android.view.MotionEvent; +import com.google.ar.sceneform.HitTestResult; +import com.google.ar.sceneform.math.Vector3; + +/** Gesture for when the user performs a two-finger pinch motion on the touch screen. */ +public class PinchGesture extends BaseGesture { + private static final String TAG = PinchGesture.class.getSimpleName(); + + /** Interface definition for callbacks to be invoked by a {@link PinchGesture}. */ + public interface OnGestureEventListener + extends BaseGesture.OnGestureEventListener {} + + private final int pointerId1; + private final int pointerId2; + private final Vector3 startPosition1; + private final Vector3 startPosition2; + private final Vector3 previousPosition1; + private final Vector3 previousPosition2; + private float gap; + private float gapDelta; + + private static final float SLOP_INCHES = 0.05f; + private static final float SLOP_MOTION_DIRECTION_DEGREES = 30.0f; + + private static final boolean PINCH_GESTURE_DEBUG = false; + + public PinchGesture( + GesturePointersUtility gesturePointersUtility, MotionEvent motionEvent, int pointerId2) { + super(gesturePointersUtility); + + pointerId1 = motionEvent.getPointerId(motionEvent.getActionIndex()); + this.pointerId2 = pointerId2; + startPosition1 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId1); + startPosition2 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId2); + previousPosition1 = new Vector3(startPosition1); + previousPosition2 = new Vector3(startPosition2); + debugLog("Created"); + } + + public float getGap() { + return gap; + } + + public float gapInches() { + return gesturePointersUtility.pixelsToInches(getGap()); + } + + public float getGapDelta() { + return gapDelta; + } + + public float gapDeltaInches() { + return gesturePointersUtility.pixelsToInches(getGapDelta()); + } + + /** Cancels the gesture in progress. */ + @Override + public void cancel() { + super.cancel(); + } + + @Override + protected boolean canStart(HitTestResult hitTestResult, MotionEvent motionEvent) { + if (gesturePointersUtility.isPointerIdRetained(pointerId1) + || gesturePointersUtility.isPointerIdRetained(pointerId2)) { + cancel(); + return false; + } + + int actionId = motionEvent.getPointerId(motionEvent.getActionIndex()); + int action = motionEvent.getActionMasked(); + + if (action == MotionEvent.ACTION_CANCEL) { + cancel(); + return false; + } + + boolean touchEnded = action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP; + + if (touchEnded && (actionId == pointerId1 || actionId == pointerId2)) { + cancel(); + return false; + } + + if (action != MotionEvent.ACTION_MOVE) { + return false; + } + + Vector3 firstToSecond = Vector3.subtract(startPosition1, startPosition2); + Vector3 firstToSecondDirection = firstToSecond.normalized(); + + Vector3 newPosition1 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId1); + Vector3 newPosition2 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId2); + Vector3 deltaPosition1 = Vector3.subtract(newPosition1, previousPosition1); + Vector3 deltaPosition2 = Vector3.subtract(newPosition2, previousPosition2); + previousPosition1.set(newPosition1); + previousPosition2.set(newPosition2); + + float dot1 = Vector3.dot(deltaPosition1.normalized(), firstToSecondDirection.negated()); + float dot2 = Vector3.dot(deltaPosition2.normalized(), firstToSecondDirection); + float dotThreshold = (float) Math.cos(Math.toRadians(SLOP_MOTION_DIRECTION_DEGREES)); + + // Check angle of motion for the first touch. + if (!Vector3.equals(deltaPosition1, Vector3.zero()) && Math.abs(dot1) < dotThreshold) { + return false; + } + + // Check angle of motion for the second touch. + if (!Vector3.equals(deltaPosition2, Vector3.zero()) && Math.abs(dot2) < dotThreshold) { + return false; + } + + float startGap = firstToSecond.length(); + gap = Vector3.subtract(newPosition1, newPosition2).length(); + float separation = Math.abs(gap - startGap); + float slopPixels = gesturePointersUtility.inchesToPixels(SLOP_INCHES); + if (separation < slopPixels) { + return false; + } + + return true; + } + + @Override + protected void onStart(HitTestResult hitTestResult, MotionEvent motionEvent) { + debugLog("Started"); + gesturePointersUtility.retainPointerId(pointerId1); + gesturePointersUtility.retainPointerId(pointerId2); + } + + @Override + protected boolean updateGesture(HitTestResult hitTestResult, MotionEvent motionEvent) { + int actionId = motionEvent.getPointerId(motionEvent.getActionIndex()); + int action = motionEvent.getActionMasked(); + + if (action == MotionEvent.ACTION_CANCEL) { + cancel(); + return false; + } + + boolean touchEnded = action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP; + + if (touchEnded && (actionId == pointerId1 || actionId == pointerId2)) { + complete(); + return false; + } + + if (action != MotionEvent.ACTION_MOVE) { + return false; + } + + Vector3 newPosition1 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId1); + Vector3 newPosition2 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId2); + float newGap = Vector3.subtract(newPosition1, newPosition2).length(); + + if (newGap == gap) { + return false; + } + + gapDelta = newGap - gap; + gap = newGap; + debugLog("Update: " + gapDelta); + return true; + } + + @Override + protected void onCancel() { + debugLog("Cancelled"); + } + + @Override + protected void onFinish() { + debugLog("Finished"); + gesturePointersUtility.releasePointerId(pointerId1); + gesturePointersUtility.releasePointerId(pointerId2); + } + + @Override + protected PinchGesture getSelf() { + return this; + } + + private static void debugLog(String log) { + if (PINCH_GESTURE_DEBUG) { + Log.d(TAG, "PinchGesture:[" + log + "]"); + } + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/PinchGestureRecognizer.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/PinchGestureRecognizer.java new file mode 100644 index 0000000..d7da310 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/PinchGestureRecognizer.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.view.MotionEvent; +import com.google.ar.sceneform.HitTestResult; + +/** Gesture Recognizer for when the user performs a two-finger pinch motion on the touch screen. */ +public class PinchGestureRecognizer extends BaseGestureRecognizer { + /** Interface definition for a callbacks to be invoked when a {@link PinchGesture} starts. */ + public interface OnGestureStartedListener + extends BaseGestureRecognizer.OnGestureStartedListener {} + + public PinchGestureRecognizer(GesturePointersUtility gesturePointersUtility) { + super(gesturePointersUtility); + } + + @Override + protected void tryCreateGestures(HitTestResult hitTestResult, MotionEvent motionEvent) { + // Pinch gestures require at least two fingers to be touching. + if (motionEvent.getPointerCount() < 2) { + return; + } + + int actionId = motionEvent.getPointerId(motionEvent.getActionIndex()); + int action = motionEvent.getActionMasked(); + boolean touchBegan = + action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN; + + if (!touchBegan || gesturePointersUtility.isPointerIdRetained(actionId)) { + return; + } + + // Determine if there is another pointer Id that has not yet been retained. + for (int i = 0; i < motionEvent.getPointerCount(); i++) { + int pointerId = motionEvent.getPointerId(i); + if (pointerId == actionId) { + continue; + } + + if (gesturePointersUtility.isPointerIdRetained(pointerId)) { + continue; + } + + gestures.add(new PinchGesture(gesturePointersUtility, motionEvent, pointerId)); + } + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/PlaneDiscoveryController.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/PlaneDiscoveryController.java new file mode 100644 index 0000000..b596471 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/PlaneDiscoveryController.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Google LLC. All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.view.View; +import androidx.annotation.Nullable; + +/** + * This view manages showing the plane discovery instructions view. You can assign into the + * planeDiscoveryView to override the default visual, or assign null to remove it. + */ +public class PlaneDiscoveryController { + @Nullable private View planeDiscoveryView; + + public PlaneDiscoveryController(@Nullable View planeDiscoveryView) { + this.planeDiscoveryView = planeDiscoveryView; + } + + /** Set the instructions view to present over the Sceneform view. */ + public void setInstructionView(View view) { + planeDiscoveryView = view; + } + + /** Show the plane discovery UX instructions for finding a plane. */ + public void show() { + if (planeDiscoveryView == null) { + return; + } + + planeDiscoveryView.setVisibility(View.VISIBLE); + } + + /** Hide the plane discovery UX instructions. */ + public void hide() { + if (planeDiscoveryView == null) { + return; + } + + planeDiscoveryView.setVisibility(View.GONE); + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/RotationController.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/RotationController.java new file mode 100644 index 0000000..a7761cd --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/RotationController.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import com.google.ar.sceneform.math.Quaternion; +import com.google.ar.sceneform.math.Vector3; + +/** + * Manipulates the rotation of a {@link BaseTransformableNode} using a {@link + * TwistGestureRecognizer}. + */ +public class RotationController extends BaseTransformationController { + + // Rate that the node rotates in degrees per degree of twisting. + private float rotationRateDegrees = 2.5f; + + public RotationController( + BaseTransformableNode transformableNode, TwistGestureRecognizer gestureRecognizer) { + super(transformableNode, gestureRecognizer); + } + + public void setRotationRateDegrees(float rotationRateDegrees) { + this.rotationRateDegrees = rotationRateDegrees; + } + + public float getRotationRateDegrees() { + return rotationRateDegrees; + } + + @Override + public boolean canStartTransformation(TwistGesture gesture) { + return getTransformableNode().isSelected(); + } + + @Override + public void onContinueTransformation(TwistGesture gesture) { + float rotationAmount = -gesture.getDeltaRotationDegrees() * rotationRateDegrees; + Quaternion rotationDelta = new Quaternion(Vector3.up(), rotationAmount); + Quaternion localrotation = getTransformableNode().getLocalRotation(); + localrotation = Quaternion.multiply(localrotation, rotationDelta); + getTransformableNode().setLocalRotation(localrotation); + } + + @Override + public void onEndTransformation(TwistGesture gesture) {} +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/ScaleController.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/ScaleController.java new file mode 100644 index 0000000..5c3ac45 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/ScaleController.java @@ -0,0 +1,155 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import com.google.ar.sceneform.FrameTime; +import com.google.ar.sceneform.Node; +import com.google.ar.sceneform.math.MathHelper; +import com.google.ar.sceneform.math.Vector3; + +/** + * Manipulates the Scale of a {@link BaseTransformableNode} using a Pinch {@link + * PinchGestureRecognizer}. Applies a tunable elastic bounce-back when scaling the {@link + * BaseTransformableNode} beyond the min/max scale. + */ +public class ScaleController extends BaseTransformationController { + public static final float DEFAULT_MIN_SCALE = 0.75f; + public static final float DEFAULT_MAX_SCALE = 1.75f; + public static final float DEFAULT_SENSITIVITY = 0.75f; + public static final float DEFAULT_ELASTICITY = 0.15f; + + private float minScale = DEFAULT_MIN_SCALE; + private float maxScale = DEFAULT_MAX_SCALE; + private float sensitivity = DEFAULT_SENSITIVITY; + private float elasticity = DEFAULT_ELASTICITY; + + private float currentScaleRatio; + + private static final float ELASTIC_RATIO_LIMIT = 0.8f; + private static final float LERP_SPEED = 8.0f; + + public ScaleController( + BaseTransformableNode transformableNode, PinchGestureRecognizer gestureRecognizer) { + super(transformableNode, gestureRecognizer); + } + + public void setMinScale(float minScale) { + this.minScale = minScale; + } + + public float getMinScale() { + return minScale; + } + + public void setMaxScale(float maxScale) { + this.maxScale = maxScale; + } + + public float getMaxScale() { + return maxScale; + } + + public void setSensitivity(float sensitivity) { + this.sensitivity = sensitivity; + } + + public float getSensitivity() { + return sensitivity; + } + + public void setElasticity(float elasticity) { + this.elasticity = elasticity; + } + + public float getElasticity() { + return elasticity; + } + + @Override + public void onActivated(Node node) { + super.onActivated(node); + Vector3 scale = getTransformableNode().getLocalScale(); + currentScaleRatio = (scale.x - minScale) / getScaleDelta(); + } + + @Override + public void onUpdated(Node node, FrameTime frameTime) { + if (isTransforming() || !isEnabled()) { + return; + } + + float t = MathHelper.clamp(frameTime.getDeltaSeconds() * LERP_SPEED, 0, 1); + currentScaleRatio = MathHelper.lerp(currentScaleRatio, getClampedScaleRatio(), t); + float finalScaleValue = getFinalScale(); + Vector3 finalScale = new Vector3(finalScaleValue, finalScaleValue, finalScaleValue); + getTransformableNode().setLocalScale(finalScale); + } + + @Override + public boolean canStartTransformation(PinchGesture gesture) { + return getTransformableNode().isSelected(); + } + + @Override + public void onContinueTransformation(PinchGesture gesture) { + currentScaleRatio += gesture.gapDeltaInches() * sensitivity; + + float finalScaleValue = getFinalScale(); + Vector3 finalScale = new Vector3(finalScaleValue, finalScaleValue, finalScaleValue); + getTransformableNode().setLocalScale(finalScale); + + if (currentScaleRatio < -ELASTIC_RATIO_LIMIT + || currentScaleRatio > (1.0f + ELASTIC_RATIO_LIMIT)) { + gesture.cancel(); + } + } + + @Override + public void onEndTransformation(PinchGesture gesture) {} + + private float getScaleDelta() { + float scaleDelta = maxScale - minScale; + + if (scaleDelta <= 0.0f) { + throw new IllegalStateException("maxScale must be greater than minScale."); + } + + return scaleDelta; + } + + private float getClampedScaleRatio() { + return Math.min(1.0f, Math.max(0.0f, currentScaleRatio)); + } + + private float getFinalScale() { + float elasticScaleRatio = getClampedScaleRatio() + getElasticDelta(); + float elasticScale = minScale + elasticScaleRatio * getScaleDelta(); + return elasticScale; + } + + private float getElasticDelta() { + float overRatio; + if (currentScaleRatio > 1.0f) { + overRatio = currentScaleRatio - 1.0f; + } else if (currentScaleRatio < 0.0f) { + overRatio = currentScaleRatio; + } else { + return 0.0f; + } + + return (1.0f - (1.0f / ((Math.abs(overRatio) * elasticity) + 1.0f))) * Math.signum(overRatio); + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/SelectionVisualizer.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/SelectionVisualizer.java new file mode 100644 index 0000000..a34acbd --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/SelectionVisualizer.java @@ -0,0 +1,28 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +/** Interface to visual when a {@link BaseTransformableNode} is selected. */ +public interface SelectionVisualizer { + + /** Adds a visual that indicates a {@link BaseTransformableNode} is currently selected. */ + void applySelectionVisual(BaseTransformableNode node); + + /** + * Removes the visual that was indicating a {@link BaseTransformableNode} is currently selected. + */ + void removeSelectionVisual(BaseTransformableNode node); +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TransformableNode.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TransformableNode.java new file mode 100644 index 0000000..8ff935e --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TransformableNode.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +/** + * Node that can be selected, translated, rotated, and scaled using gestures from {@link + * TransformationSystem}. + */ +public class TransformableNode extends BaseTransformableNode { + private final TranslationController translationController; + private final ScaleController scaleController; + private final RotationController rotationController; + + @SuppressWarnings("initialization") // Suppress @UnderInitialization warning. + public TransformableNode(TransformationSystem transformationSystem) { + super(transformationSystem); + + translationController = + new TranslationController(this, transformationSystem.getDragRecognizer()); + addTransformationController(translationController); + + scaleController = new ScaleController(this, transformationSystem.getPinchRecognizer()); + addTransformationController(scaleController); + + rotationController = new RotationController(this, transformationSystem.getTwistRecognizer()); + addTransformationController(rotationController); + } + + /** Returns the controller that translates this node using a drag gesture. */ + public TranslationController getTranslationController() { + return translationController; + } + + /** Returns the controller that scales this node using a pinch gesture. */ + public ScaleController getScaleController() { + return scaleController; + } + + /** Returns the controller that rotates this node using a twist gesture. */ + public RotationController getRotationController() { + return rotationController; + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TransformationSystem.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TransformationSystem.java new file mode 100644 index 0000000..6e36e48 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TransformationSystem.java @@ -0,0 +1,184 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import androidx.annotation.Nullable; +import com.google.ar.sceneform.HitTestResult; +import java.util.ArrayList; + +/** + * Coordinates which {@link BaseTransformableNode} is currently selected. Also, detects various + * gestures used by the transformation controls of {@link BaseTransformableNode}. + * + *

{@link #onTouch(HitTestResult, MotionEvent)} must be called for gestures to be detected. By + * default, this is done automatically by {@link ArFragment}. + */ +public class TransformationSystem { + private final GesturePointersUtility gesturePointersUtility; + + private final DragGestureRecognizer dragGestureRecognizer; + private final PinchGestureRecognizer pinchGestureRecognizer; + private final TwistGestureRecognizer twistGestureRecognizer; + + private final ArrayList> recognizers = new ArrayList<>(); + + private SelectionVisualizer selectionVisualizer; + + @Nullable private BaseTransformableNode selectedNode; + + @SuppressWarnings("initialization") + public TransformationSystem( + DisplayMetrics displayMetrics, SelectionVisualizer selectionVisualizer) { + this.selectionVisualizer = selectionVisualizer; + + gesturePointersUtility = new GesturePointersUtility(displayMetrics); + + dragGestureRecognizer = new DragGestureRecognizer(gesturePointersUtility); + addGestureRecognizer(dragGestureRecognizer); + + pinchGestureRecognizer = new PinchGestureRecognizer(gesturePointersUtility); + addGestureRecognizer(pinchGestureRecognizer); + + twistGestureRecognizer = new TwistGestureRecognizer(gesturePointersUtility); + addGestureRecognizer(twistGestureRecognizer); + } + + /** + * Sets the selection visualizer used to visualize which {@link BaseTransformableNode} is + * currently selected. If there is already a selected node, then the old selection visual is + * removed and the new one is applied immediately. + */ + public void setSelectionVisualizer(SelectionVisualizer selectionVisualizer) { + if (selectedNode != null) { + this.selectionVisualizer.removeSelectionVisual(selectedNode); + } + + this.selectionVisualizer = selectionVisualizer; + + if (selectedNode != null) { + this.selectionVisualizer.applySelectionVisual(selectedNode); + } + } + + /** + * Gets the selection visualizer used to visualize which {@link BaseTransformableNode} is + * currently selected. + */ + public SelectionVisualizer getSelectionVisualizer() { + return selectionVisualizer; + } + + /** + * Gets the utility used by {@link BaseGestureRecognizer} subclasses to retain/release pointer Ids + * so that each pointer can only be used in one gesture at a time. + */ + public GesturePointersUtility getGesturePointersUtility() { + return gesturePointersUtility; + } + + /** + * Gets the gesture recognizer for determining when the user performs a drag motion on the touch + * screen. + */ + public DragGestureRecognizer getDragRecognizer() { + return dragGestureRecognizer; + } + + /** + * Gets the gesture recognizer for determining when the user performs a two-finger pinch motion on + * the touch screen. + */ + public PinchGestureRecognizer getPinchRecognizer() { + return pinchGestureRecognizer; + } + + /** + * Gets the gesture recognizer for determining when the user performs a two-finger twist motion on + * the touch screen. + */ + public TwistGestureRecognizer getTwistRecognizer() { + return twistGestureRecognizer; + } + + /** + * Adds a gesture recognizer to this transformation system. Touch events will be dispatched to the + * recognizer when {@link #onTouch(HitTestResult, MotionEvent)} is called. + */ + public void addGestureRecognizer(BaseGestureRecognizer gestureRecognizer) { + recognizers.add(gestureRecognizer); + } + + /** + * Gets the currently selected node. Only the currently selected node can be transformed. Nodes + * are selected automatically when they are tapped, or when the user begins to translate the node + * with a drag gesture. + */ + @Nullable + public BaseTransformableNode getSelectedNode() { + return selectedNode; + } + + /** + * Sets a {@link BaseTransformableNode} as the selected node if there is no currently selected + * node or if the currently selected node is not actively being transformed. If null, then + * deselects the currently selected node if the node is not transforming. + * + * @see BaseTransformableNode#isTransforming + * @return true if the node was successfully selected + */ + public boolean selectNode(@Nullable BaseTransformableNode node) { + if (!deselectNode()) { + return false; + } + + if (node != null) { + selectedNode = node; + selectionVisualizer.applySelectionVisual(selectedNode); + } + + return true; + } + + /** Dispatches touch events to the gesture recognizers contained by this transformation system. */ + public void onTouch(HitTestResult hitTestResult, MotionEvent motionEvent) { + for (int i = 0; i < recognizers.size(); i++) { + recognizers.get(i).onTouch(hitTestResult, motionEvent); + } + } + + /** + * Deselects the currently selected node if the node is not currently transforming. + * + * @see BaseTransformableNode#isTransforming + * @return true if the node was successfully deselected + */ + private boolean deselectNode() { + if (selectedNode == null) { + return true; + } + + if (selectedNode.isTransforming()) { + return false; + } + + selectionVisualizer.removeSelectionVisual(selectedNode); + selectedNode = null; + + return true; + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TranslationController.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TranslationController.java new file mode 100644 index 0000000..6475849 --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TranslationController.java @@ -0,0 +1,277 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import androidx.annotation.Nullable; +import com.google.ar.core.Anchor; +import com.google.ar.core.Camera; +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.AnchorNode; +import com.google.ar.sceneform.ArSceneView; +import com.google.ar.sceneform.FrameTime; +import com.google.ar.sceneform.Node; +import com.google.ar.sceneform.Scene; +import com.google.ar.sceneform.math.MathHelper; +import com.google.ar.sceneform.math.Quaternion; +import com.google.ar.sceneform.math.Vector3; +import com.google.ar.sceneform.utilities.Preconditions; +import java.util.EnumSet; +import java.util.List; + +/** + * Manipulates the position of a {@link BaseTransformableNode} using a {@link + * DragGestureRecognizer}. If not selected, the {@link BaseTransformableNode} will become selected + * when the {@link DragGesture} starts. + */ +public class TranslationController extends BaseTransformationController { + @Nullable private HitResult lastArHitResult; + @Nullable private Vector3 desiredLocalPosition; + @Nullable private Quaternion desiredLocalRotation; + + private final Vector3 initialForwardInLocal = new Vector3(); + + private EnumSet allowedPlaneTypes = EnumSet.allOf(Plane.Type.class); + + private static final float LERP_SPEED = 12.0f; + private static final float POSITION_LENGTH_THRESHOLD = 0.01f; + private static final float ROTATION_DOT_THRESHOLD = 0.99f; + + public TranslationController( + BaseTransformableNode transformableNode, DragGestureRecognizer gestureRecognizer) { + super(transformableNode, gestureRecognizer); + } + + /** Sets which types of ArCore Planes this TranslationController is allowed to translate on. */ + public void setAllowedPlaneTypes(EnumSet allowedPlaneTypes) { + this.allowedPlaneTypes = allowedPlaneTypes; + } + + /** + * Gets a reference to the EnumSet that determines which types of ArCore Planes this + * TranslationController is allowed to translate on. + */ + public EnumSet getAllowedPlaneTypes() { + return allowedPlaneTypes; + } + + @Override + public void onUpdated(Node node, FrameTime frameTime) { + updatePosition(frameTime); + updateRotation(frameTime); + } + + @Override + public boolean isTransforming() { + // As long as the transformable node is still interpolating towards the final pose, this + // controller is still transforming. + return super.isTransforming() || desiredLocalRotation != null || desiredLocalPosition != null; + } + + @Override + public boolean canStartTransformation(DragGesture gesture) { + Node targetNode = gesture.getTargetNode(); + if (targetNode == null) { + return false; + } + + BaseTransformableNode transformableNode = getTransformableNode(); + if (targetNode != transformableNode && !targetNode.isDescendantOf(transformableNode)) { + return false; + } + + if (!transformableNode.isSelected() && !transformableNode.select()) { + return false; + } + + Vector3 initialForwardInWorld = transformableNode.getForward(); + Node parent = transformableNode.getParent(); + if (parent != null) { + initialForwardInLocal.set(parent.worldToLocalDirection(initialForwardInWorld)); + } else { + initialForwardInLocal.set(initialForwardInWorld); + } + + return true; + } + + @Override + public void onContinueTransformation(DragGesture gesture) { + Scene scene = getTransformableNode().getScene(); + if (scene == null) { + return; + } + + Frame frame = ((ArSceneView) scene.getView()).getArFrame(); + if (frame == null) { + return; + } + + Camera arCamera = frame.getCamera(); + if (arCamera.getTrackingState() != TrackingState.TRACKING) { + return; + } + + Vector3 position = gesture.getPosition(); + List hitResultList = frame.hitTest(position.x, position.y); + for (int i = 0; i < hitResultList.size(); i++) { + HitResult hit = hitResultList.get(i); + Trackable trackable = hit.getTrackable(); + Pose pose = hit.getHitPose(); + if (trackable instanceof Plane) { + Plane plane = (Plane) trackable; + if (plane.isPoseInPolygon(pose) && allowedPlaneTypes.contains(plane.getType())) { + desiredLocalPosition = new Vector3(pose.tx(), pose.ty(), pose.tz()); + desiredLocalRotation = new Quaternion(pose.qx(), pose.qy(), pose.qz(), pose.qw()); + Node parent = getTransformableNode().getParent(); + if (parent != null && desiredLocalPosition != null && desiredLocalRotation != null) { + desiredLocalPosition = parent.worldToLocalPoint(desiredLocalPosition); + desiredLocalRotation = + Quaternion.multiply( + parent.getWorldRotation().inverted(), + Preconditions.checkNotNull(desiredLocalRotation)); + } + + desiredLocalRotation = + calculateFinalDesiredLocalRotation(Preconditions.checkNotNull(desiredLocalRotation)); + lastArHitResult = hit; + break; + } + } + } + } + + @Override + public void onEndTransformation(DragGesture gesture) { + HitResult hitResult = lastArHitResult; + if (hitResult == null) { + return; + } + + if (hitResult.getTrackable().getTrackingState() == TrackingState.TRACKING) { + AnchorNode anchorNode = getAnchorNodeOrDie(); + + Anchor oldAnchor = anchorNode.getAnchor(); + if (oldAnchor != null) { + oldAnchor.detach(); + } + + Anchor newAnchor = hitResult.createAnchor(); + + Vector3 worldPosition = getTransformableNode().getWorldPosition(); + Quaternion worldRotation = getTransformableNode().getWorldRotation(); + Quaternion finalDesiredWorldRotation = worldRotation; + + // Since we change the anchor, we need to update the initialForwardInLocal into the new + // coordinate space. Local variable for nullness analysis. + Quaternion desiredLocalRotation = this.desiredLocalRotation; + if (desiredLocalRotation != null) { + getTransformableNode().setLocalRotation(desiredLocalRotation); + finalDesiredWorldRotation = getTransformableNode().getWorldRotation(); + } + + anchorNode.setAnchor(newAnchor); + + // Temporarily set the node to the final world rotation so that we can accurately + // determine the initialForwardInLocal in the new coordinate space. + getTransformableNode().setWorldRotation(finalDesiredWorldRotation); + Vector3 initialForwardInWorld = getTransformableNode().getForward(); + initialForwardInLocal.set(anchorNode.worldToLocalDirection(initialForwardInWorld)); + + getTransformableNode().setWorldRotation(worldRotation); + getTransformableNode().setWorldPosition(worldPosition); + } + + desiredLocalPosition = Vector3.zero(); + desiredLocalRotation = calculateFinalDesiredLocalRotation(Quaternion.identity()); + } + + private AnchorNode getAnchorNodeOrDie() { + Node parent = getTransformableNode().getParent(); + if (!(parent instanceof AnchorNode)) { + throw new IllegalStateException("TransformableNode must have an AnchorNode as a parent."); + } + + return (AnchorNode) parent; + } + + private void updatePosition(FrameTime frameTime) { + // Store in local variable for nullness static analysis. + Vector3 desiredLocalPosition = this.desiredLocalPosition; + if (desiredLocalPosition == null) { + return; + } + + Vector3 localPosition = getTransformableNode().getLocalPosition(); + float lerpFactor = MathHelper.clamp(frameTime.getDeltaSeconds() * LERP_SPEED, 0, 1); + localPosition = Vector3.lerp(localPosition, desiredLocalPosition, lerpFactor); + + float lengthDiff = Math.abs(Vector3.subtract(desiredLocalPosition, localPosition).length()); + if (lengthDiff <= POSITION_LENGTH_THRESHOLD) { + localPosition = desiredLocalPosition; + this.desiredLocalPosition = null; + } + + getTransformableNode().setLocalPosition(localPosition); + } + + private void updateRotation(FrameTime frameTime) { + // Store in local variable for nullness static analysis. + Quaternion desiredLocalRotation = this.desiredLocalRotation; + if (desiredLocalRotation == null) { + return; + } + + Quaternion localRotation = getTransformableNode().getLocalRotation(); + float lerpFactor = MathHelper.clamp(frameTime.getDeltaSeconds() * LERP_SPEED, 0, 1); + localRotation = Quaternion.slerp(localRotation, desiredLocalRotation, lerpFactor); + + float dot = Math.abs(dotQuaternion(localRotation, desiredLocalRotation)); + if (dot >= ROTATION_DOT_THRESHOLD) { + localRotation = desiredLocalRotation; + this.desiredLocalRotation = null; + } + + getTransformableNode().setLocalRotation(localRotation); + } + + /** + * When translating, the up direction of the node must match the up direction of the plane from + * the hit result. However, we also need to make sure that the original forward direction of the + * node is respected. + */ + private Quaternion calculateFinalDesiredLocalRotation(Quaternion desiredLocalRotation) { + // Get a rotation just to the up direction. + // Otherwise, the node will spin around as you rotate. + Vector3 rotatedUp = Quaternion.rotateVector(desiredLocalRotation, Vector3.up()); + desiredLocalRotation = Quaternion.rotationBetweenVectors(Vector3.up(), rotatedUp); + + // Adjust the rotation to make sure the node maintains the same forward direction. + Quaternion forwardInLocal = + Quaternion.rotationBetweenVectors(Vector3.forward(), initialForwardInLocal); + desiredLocalRotation = Quaternion.multiply(desiredLocalRotation, forwardInLocal); + + return desiredLocalRotation.normalized(); + } + + private static float dotQuaternion(Quaternion lhs, Quaternion rhs) { + return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w; + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TwistGesture.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TwistGesture.java new file mode 100644 index 0000000..99d0f3a --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TwistGesture.java @@ -0,0 +1,182 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.util.Log; +import android.view.MotionEvent; +import com.google.ar.sceneform.HitTestResult; +import com.google.ar.sceneform.math.Vector3; + +/** Gesture for when the user performs a two-finger twist motion on the touch screen. */ +public class TwistGesture extends BaseGesture { + private static final String TAG = TwistGesture.class.getSimpleName(); + + /** Interface definition for callbacks to be invoked by a {@link TwistGesture}. */ + public interface OnGestureEventListener + extends BaseGesture.OnGestureEventListener {} + + private static final boolean TWIST_GESTURE_DEBUG = false; + + private final int pointerId1; + private final int pointerId2; + private final Vector3 startPosition1; + private final Vector3 startPosition2; + private final Vector3 previousPosition1; + private final Vector3 previousPosition2; + private float deltaRotationDegrees; + + private static final float SLOP_ROTATION_DEGREES = 15.0f; + + public TwistGesture( + GesturePointersUtility gesturePointersUtility, MotionEvent motionEvent, int pointerId2) { + super(gesturePointersUtility); + + pointerId1 = motionEvent.getPointerId(motionEvent.getActionIndex()); + this.pointerId2 = pointerId2; + startPosition1 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId1); + startPosition2 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId2); + previousPosition1 = new Vector3(startPosition1); + previousPosition2 = new Vector3(startPosition2); + debugLog("Created"); + } + + public float getDeltaRotationDegrees() { + return deltaRotationDegrees; + } + + @Override + protected boolean canStart(HitTestResult hitTestResult, MotionEvent motionEvent) { + if (gesturePointersUtility.isPointerIdRetained(pointerId1) + || gesturePointersUtility.isPointerIdRetained(pointerId2)) { + cancel(); + return false; + } + + int actionId = motionEvent.getPointerId(motionEvent.getActionIndex()); + int action = motionEvent.getActionMasked(); + + if (action == MotionEvent.ACTION_CANCEL) { + cancel(); + return false; + } + + boolean touchEnded = action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP; + + if (touchEnded && (actionId == pointerId1 || actionId == pointerId2)) { + cancel(); + return false; + } + + if (action != MotionEvent.ACTION_MOVE) { + return false; + } + + Vector3 newPosition1 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId1); + Vector3 newPosition2 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId2); + Vector3 deltaPosition1 = Vector3.subtract(newPosition1, previousPosition1); + Vector3 deltaPosition2 = Vector3.subtract(newPosition2, previousPosition2); + previousPosition1.set(newPosition1); + previousPosition2.set(newPosition2); + + // Check that both fingers are moving. + if (Vector3.equals(deltaPosition1, Vector3.zero()) + || Vector3.equals(deltaPosition2, Vector3.zero())) { + return false; + } + + float rotation = + calculateDeltaRotation(newPosition1, newPosition2, startPosition1, startPosition2); + if (Math.abs(rotation) < SLOP_ROTATION_DEGREES) { + return false; + } + + return true; + } + + @Override + protected void onStart(HitTestResult hitTestResult, MotionEvent motionEvent) { + debugLog("Started"); + gesturePointersUtility.retainPointerId(pointerId1); + gesturePointersUtility.retainPointerId(pointerId2); + } + + @Override + protected boolean updateGesture(HitTestResult hitTestResult, MotionEvent motionEvent) { + int actionId = motionEvent.getPointerId(motionEvent.getActionIndex()); + int action = motionEvent.getActionMasked(); + + if (action == MotionEvent.ACTION_CANCEL) { + cancel(); + return false; + } + + boolean touchEnded = action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP; + + if (touchEnded && (actionId == pointerId1 || actionId == pointerId2)) { + complete(); + return false; + } + + if (action != MotionEvent.ACTION_MOVE) { + return false; + } + + Vector3 newPosition1 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId1); + Vector3 newPosition2 = GesturePointersUtility.motionEventToPosition(motionEvent, pointerId2); + deltaRotationDegrees = + calculateDeltaRotation(newPosition1, newPosition2, previousPosition1, previousPosition2); + previousPosition1.set(newPosition1); + previousPosition2.set(newPosition2); + debugLog("Update: " + deltaRotationDegrees); + return true; + } + + @Override + protected void onCancel() { + debugLog("Cancelled"); + } + + @Override + protected void onFinish() { + debugLog("Finished"); + gesturePointersUtility.releasePointerId(pointerId1); + gesturePointersUtility.releasePointerId(pointerId2); + } + + @Override + protected TwistGesture getSelf() { + return this; + } + + private static void debugLog(String log) { + if (TWIST_GESTURE_DEBUG) { + Log.d(TAG, "TwistGesture:[" + log + "]"); + } + } + + private static float calculateDeltaRotation( + Vector3 currentPosition1, + Vector3 currentPosition2, + Vector3 previousPosition1, + Vector3 previousPosition2) { + Vector3 currentDirection = Vector3.subtract(currentPosition1, currentPosition2).normalized(); + Vector3 previousDirection = Vector3.subtract(previousPosition1, previousPosition2).normalized(); + float sign = + Math.signum( + previousDirection.x * currentDirection.y - previousDirection.y * currentDirection.x); + return Vector3.angleBetweenVectors(currentDirection, previousDirection) * sign; + } +} diff --git a/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TwistGestureRecognizer.java b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TwistGestureRecognizer.java new file mode 100644 index 0000000..ad7a59d --- /dev/null +++ b/sceneformux/ux/src/main/java/com/google/ar/sceneform/ux/TwistGestureRecognizer.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google LLC All Rights Reserved. + * + * 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. + */ +package com.google.ar.sceneform.ux; + +import android.view.MotionEvent; +import com.google.ar.sceneform.HitTestResult; + +/** Gesture Recognizer for when the user performs a two-finger twist motion on the touch screen. */ +public class TwistGestureRecognizer extends BaseGestureRecognizer { + /** Interface definition for a callbacks to be invoked when a {@link TwistGesture} starts. */ + public interface OnGestureStartedListener + extends BaseGestureRecognizer.OnGestureStartedListener {} + + public TwistGestureRecognizer(GesturePointersUtility gesturePointersUtility) { + super(gesturePointersUtility); + } + + @Override + protected void tryCreateGestures(HitTestResult hitTestResult, MotionEvent motionEvent) { + // Twist gestures require at least two fingers to be touching. + if (motionEvent.getPointerCount() < 2) { + return; + } + + int actionId = motionEvent.getPointerId(motionEvent.getActionIndex()); + int action = motionEvent.getActionMasked(); + boolean touchBegan = + action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN; + + if (!touchBegan || gesturePointersUtility.isPointerIdRetained(actionId)) { + return; + } + + // Determine if there is another pointer Id that has not yet been retained. + for (int i = 0; i < motionEvent.getPointerCount(); i++) { + int pointerId = motionEvent.getPointerId(i); + if (pointerId == actionId) { + continue; + } + + if (gesturePointersUtility.isPointerIdRetained(pointerId)) { + continue; + } + + gestures.add(new TwistGesture(gesturePointersUtility, motionEvent, pointerId)); + } + } +} diff --git a/sceneformux/ux/src/main/res/drawable/sceneform_hand_phone.png b/sceneformux/ux/src/main/res/drawable/sceneform_hand_phone.png new file mode 100644 index 0000000..5608732 Binary files /dev/null and b/sceneformux/ux/src/main/res/drawable/sceneform_hand_phone.png differ diff --git a/sceneformux/ux/src/main/res/layout/sceneform_plane_discovery_layout.xml b/sceneformux/ux/src/main/res/layout/sceneform_plane_discovery_layout.xml new file mode 100644 index 0000000..1568b35 --- /dev/null +++ b/sceneformux/ux/src/main/res/layout/sceneform_plane_discovery_layout.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/sceneformux/ux/src/main/res/layout/sceneform_ux_fragment_layout.xml b/sceneformux/ux/src/main/res/layout/sceneform_ux_fragment_layout.xml new file mode 100644 index 0000000..3b8dca7 --- /dev/null +++ b/sceneformux/ux/src/main/res/layout/sceneform_ux_fragment_layout.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/sceneformux/ux/src/main/res/raw/sceneform_footprint.glb b/sceneformux/ux/src/main/res/raw/sceneform_footprint.glb new file mode 100644 index 0000000..1bd972c Binary files /dev/null and b/sceneformux/ux/src/main/res/raw/sceneform_footprint.glb differ diff --git a/sceneformux/wrapper/gradle-wrapper.properties b/sceneformux/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bcedaa3 --- /dev/null +++ b/sceneformux/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Nov 20 10:27:45 PST 2017 +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 diff --git a/settings.gradle b/settings.gradle index 40148b8..3f4ad79 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,8 @@ include ':app' +include ':sceneform' +project(':sceneform').projectDir=new File('sceneformsrc/sceneform') + +include ':sceneformux' +project(':sceneformux').projectDir=new File('sceneformux/ux') + rootProject.name='MagMoleculAr'