There is a huge demand these days for Android apps that offer immersive virtual reality or augmented reality experiences. As a developer, there are many different frameworks you can use to create such apps.
But, unless you are also a skilled 3D artist, how are you going to create the 3D objects you would be displaying in those apps? Are you ready to spend months learning how to work with 3D modeling programs such as Blender or Maya? If you are not, you should consider using Google Poly, an online repository containing thousands of 3D assets that come with Creative Commons licenses.
Most of the assets you can find on Poly today are low poly with simple materials. This is because the average mobile GPU is yet to become powerful enough to display 3D objects with high polygon counts in real time.
In this tutorial, I'll introduce you to the Poly API. I'll also show you how to use Processing for Android to render the 3D assets you download.
Prerequisites
To make the most of this tutorial, you'll need:
- the latest version of Android Studio
- a device running Android API level 21 or higher
- and a Google Cloud account
1. Acquiring an API Key
All the HTTP requests you make to the Poly API must be accompanied by an API key that belongs to you. To acquire the key, start by logging in to the Google Cloud console and navigating to the APIs dashboard.
Next, press the Enable APIs and services button, expand the Other category, and select Poly API.
You can now press the Enable button to enable the Poly API.
Once the API is enabled, a set of API keys are generated for it automatically. You can open the Credentials tab to take a look at them.
For this tutorial, you'll be needing only the Android key. Make a note of it so you can use it later.
2. Project Setup
Because Poly currently doesn't have an official toolkit for the Android platform, you'll have to work with the Poly API directly using its REST interface. By using the Fuel networking library, which is optimized for the Kotlin language, you can save a lot of time and effort. So add the following implementation
dependency in the app
module's build.gradle file:
implementation 'com.github.kittinunf.fuel:fuel-android:1.13.0'
To be able to display the 3D assets you download from the Poly repository, you'll also need a rendering engine. Processing for Android comes with one, so add it as another dependency.
implementation 'org.p5android:processing-core:4.0.1'
Lastly, don't forget to request the INTERNET
permission in the manifest file of your project.
<uses-permission android:name="android.permission.INTERNET"/>
3. Listing Assets
To be able to download a Poly asset, you must know its unique ID. By using a browser, one that supports WebGL, you can easily determine the ID of any asset. It's right in the address bar.
However, if you want to allow your users to dynamically, at run time, decide which assets they want to use, you can use the assets.list
REST method to determine the IDs of those assets. The method allows you to look for assets using a variety of parameters, such as keywords, categories, and 3D file formats.
For the sake of a realistic example, let's now try to find the IDs of a few assets that belong to the animals
category. You are, of course, free to choose any other valid category, such as architecture
, food
, or people
.
Before you compose your HTTP request, it's a good idea to declare your API key and the base URL of the Poly API as constants inside your activity.
companion object { const val key = "Abcdefghabcdefgh1234567810" const val baseURL = "https://poly.googleapis.com/v1" }
Using the base URL, you can build the URL of the assets.list
REST method as shown below:
val listURL = "$baseURL/assets"
At this point, you can create a valid HTTP GET request by calling the httpGet()
method and passing your API key and the desired category as query parameters to it. Optionally, you can use the format
query parameter to specify the desired format of the assets. Poly supports OBJ, FBX, TILT, and several other popular 3D formats.
Because the method runs asynchronously and its result is a JSON document, you must attach an event handler to it by using the responseJSON()
method. The following code shows you how:
listURL.httpGet(listOf( "category" to "animals", "key" to key, "format" to "OBJ" )).responseJson { _, _, result -> // More code here }
Inside the event handler, if your request was successful, you'll have access to a list of Asset
objects. The name
field of each such object specifies its ID.
Additionally, each object will have fields such as displayName
, license
, and authorName
, which you might find useful. For now, let's simply print the name
and displayName
of all the objects. The following code shows you how:
result.fold({ // Get assets array val assets = it.obj().getJSONArray("assets") // Loop through array for(i in 0 until assets.length()) { // Get id and displayName val id = assets.getJSONObject(i).getString("name") val displayName = assets.getJSONObject(i).getString("displayName") // Print id and displayName Log.d("POLY", "(ID: $id) -- (NAME: $displayName)") } }, { // In case of an error Log.e("POLY", "An error occurred") })
If you run your app now, you should be able to see the following output in the Logcat window of Android Studio.
4. Downloading Assets
Once you have the unique ID of an asset, you can directly append it to the base URL of the Poly API to create an asset URL.
// some asset id val assetID = "assets/3yiIERrKNQr" // its url val assetURL = "$baseURL/$assetID"
When you make an HTTP GET request to the asset URL using the httpGet()
method again, you'll get a JSON document containing just one Asset
object.
assetURL.httpGet(listOf("key" to key)) .responseJson { _, _, result -> result.fold({ val asset = it.obj() // More code here }, { Log.e("POLY", "An error occurred") }) }
As you may have noticed in the above code, this request too must have a query parameter mentioning your API key.
You've already learned how to use some of the fields present in the Asset
object in the previous step. Now, all you need to do is use the formats
array present in the object to determine the URLs and names of the files associated with the asset. Each item in the array will have three important fields:
formatType
, which lets you determine the type of the assetroot
, which contains the name and URL of the primary file associated with the assetresources
, which contains details about all the secondary files associated with the asset, such as materials and textures
If you're working with the OBJ format, the primary file will be a .obj file containing vertices and faces data, and the secondary files will usually be .mtl files containing data about the materials used. The following code shows you how to determine the URLs of both the primary and secondary files:
var objFileURL:String? = null var mtlFileURL:String? = null var mtlFileName:String? = null val formats = asset.getJSONArray("formats") // Loop through all formats for(i in 0 until formats.length()) { val currentFormat = formats.getJSONObject(i) // Check if current format is OBJ if(currentFormat.getString("formatType") == "OBJ") { // Get .obj file details objFileURL = currentFormat.getJSONObject("root") .getString("url") // Get the first .mtl file details mtlFileURL = currentFormat.getJSONArray("resources") .getJSONObject(0) .getString("url") mtlFileName = currentFormat.getJSONArray("resources") .getJSONObject(0) .getString("relativePath") break } }
In the above code, in addition to the URL of the .mtl file, we're also determining its name using the relativePath
field. Doing so is important because the name is hard coded into the mtllib
element of the .obj file and should not be changed.
Once you have the URLs of both the files, you can use the httpDownload()
method of the Fuel library to download them. Here's how you can download them to your app's private storage directory, whose absolute path can be determined using the filesDir
property:
// download and store obj file as asset.obj objFileURL!!.httpDownload().destination { _, _ -> File(filesDir, "asset.obj") }.response { _, _, result -> result.fold({}, { Log.e("POLY", "An error occurred") }) } // download and store mtl file without // changing its name mtlFileURL!!.httpDownload().destination { _, _ -> File(filesDir, mtlFileName) }.response { _, _, result -> result.fold({}, { Log.e("POLY", "An error occurred") }) }
5. Displaying Assets
You'll need a 3D canvas to draw the Poly asset you downloaded. To create one, you must extend the PApplet
class offered by the Processing for Android library. However, a canvas created this way will, by default, support only 2D shapes. To configure it to draw 3D shapes as well, override the settings()
method and pass P3D
as an argument to the fullScreen()
method, which also makes the canvas as large as the user's screen.
val canvas = object : PApplet() { override fun settings() { fullScreen(PConstants.P3D) } // More code here }
Next, create a new property inside the class to represent the Poly asset as a PShape
object.
var myPolyAsset: PShape? = null
To initialize the property, override the setup()
method and call the loadShape()
method, passing the absolute path of the .obj file you downloaded as an argument to it.
override fun setup() { myPolyAsset = loadShape(File(filesDir, "asset.obj").absolutePath) }
You can now start drawing on the canvas by overriding the draw()
method. Inside the method, the first thing you need to do is call the background()
method to ensure that you are always drawing on a blank canvas.
override fun draw() { background(0) // More code here }
When drawn directly, most Poly assets look very small and inverted. You can fix this either by using a custom camera or by using canvas transformations. To keep this tutorial simple and intuitive, let's use canvas transformations.
To increase the size of the asset, use the scale()
method and pass a large negative value to it. The value must be negative to make sure that the asset is flipped vertically. Optionally, you can use the translate()
method to adjust its position along the X and Y axes. The following code shows you how:
scale(-50f) translate(-4f,-14f)
You can now go ahead and draw the asset by calling the shape()
method.
shape(myPolyAsset)
The canvas is currently not a part of your activity's view hierarchy. Therefore, if you try running your app now, you won't be able to see the asset. To fix this, first add a new FrameLayout
widget to the activity's layout XML file.
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/canvas_holder"></FrameLayout>
Then, create a new PFragment
instance using the canvas and point it to the FrameLayout
widget.
val fragment = PFragment(canvas) fragment.setView(canvas_holder, this)
At this point, you can run the app again to see the asset.
Conclusion
You now know how to use the Poly API to search for and download 3D assets. In this tutorial, you also learned how to render and manipulate those assets using Processing for Android.
It's worth noting that many of the assets available on Poly were created using Google Blocks, an application available for users of HTC Vive and Oculus Rift. If you own those VR headsets, do consider creating and submitting your own models.
To learn more about the Poly API, you can refer to the official documentation.