在 Revolut,咱們將客戶體驗置於咱們所作的一切的核心,旨在經過簡單的設計和謹慎的執行帶來愉悅。而後,當咱們介紹卡片訂單流程的更新時,您能夠想象咱們的興奮。在最新版本的 Revolut 應用程序中,您將可以從交互式3D模型中選擇您的卡。html
這對咱們來講是一個有趣的挑戰,由於這是咱們第一次使用基於3D物理的引擎來建立一個功能。咱們認爲結果很是好!java
進入應用程序的卡片訂單部分,您將能夠選擇兩種材料 - 塑料和金屬。從那裏,您將可以選擇一種顏色,以及您是否須要 Visa 或萬事達卡(取決於您所在的國家/地區)。android
讓咱們來看看咱們如何達到這個技術高度,並探索一路上的一些挑戰。git
從哪裏開始? 首先,咱們嘗試使用 GLSurfaceView,建立咱們本身的渲染器並使用 OpenGL ES 繪製卡片。但這種方法有一些缺點:github
因此咱們認爲咱們會找到更好的解決方案。一些搜索引導咱們選擇幾個方面:後端
Sceneform 支持如下格式的3D資源:bash
要將咱們的卡片模型包含到項目中,咱們須要連接咱們的資源並.sfb經過 Android Studio 插件將它們轉換爲文件。app
做爲資產的一部分,咱們應該建立本身的材料。材質定義表面的視覺外觀。它是一種着色器。框架
咱們 .mat
看起來像這樣:ide
material {
name : "Card material",
parameters : [
{
type : sampler2d,
name : baseColorMap
},
{
type : sampler2d,
name : normalMap
},
{
type : sampler2d,
name : roughnessMap
},
{
type : sampler2d,
name : metallicMap
},
{
type : sampler2d,
name : reflectanceMap
}
],
requires : [
uv0
],
shadingModel : lit,
}
fragment {
void material(inout MaterialInputs material) {
vec3 normal = texture(materialParams_normalMap, getUV0()).xyz;
material.normal = normal * 2.0 - 1.0; //bump mapping
prepareMaterial(material);
material.baseColor = texture(materialParams_baseColorMap, getUV0());
material.roughness = texture(materialParams_roughnessMap, getUV0()).r;
material.metallic = texture(materialParams_metallicMap, getUV0()).r;
material.reflectance = texture(materialParams_reflectanceMap, getUV0()).r;
}
}
複製代碼
baseColor
- 定義對象的感知顏色roughness
- 控制表面的感知光滑度。metallic
- 定義表面是金屬仍是非金屬reflectance
- 此屬性可用於控制鏡面反射強度。它隻影響非金屬表面。定義了這個,咱們爲每一個屬性的UV映射建立了紋理,你能夠在下面看到其中一個:
要建立每一個紋理,咱們使用了這個 Texture.builder()
類,您須要使用如下 Texture.Usage
常量之一傳遞資源和使用類型的來源: COLOR, NORMAL, DATA
internal fun Context.loadTexture( sourceUri: Uri, usage: Texture.Usage ): Texture.Builder =
Texture.builder()
.setSource(this, Uri.parse(uri))
.setUsage(usage)
.setSampler(
Texture.Sampler.builder()
.setMagFilter(Texture.Sampler.MagFilter.LINEAR)
.setMinFilter(Texture.Sampler.MinFilter.LINEAR_MIPMAP_LINEAR)
.build()
)
複製代碼
接下來,咱們能夠收集全部必要的紋理並將它們應用到咱們加載的卡片模型:
val cardTextures = availableTextures.map { texture -> loadTexture(texture.path, texture.usage) }
ModelRenderable.builder()
.setSource(context, Uri.parse(MODEL_SFB_PATH))
.build()
.thenApply { model ->
cardTextures.forEach { result -> model.material.setTexture(result.name, result.texture) }
}
複製代碼
就是這樣!如今咱們準備創建本身的場景。
限定 layout.xml
<com.google.ar.sceneform.SceneView android:id="@+id/sceneView" android:layout_width="match_parent" android:layout_height="match_parent" />
複製代碼
並將卡節點添加到現有場景:
private val card3dNode = Node().apply {
localPosition = Vector3(CARD_POSITION_X_AXIS, CARD_POSITION_Y_AXIS, CARD_POSITION_Z_AXIS)
localRotation = getRotationQuaternion(CARD_STARTING_Y_AXIS_ANGLE.toFloat())
name = CARD_ID
}
fun addCardToScene(modelRenderable: ModelRenderable, currentCard: CardRender) {
modelRenderable.material = currentCard.value
with(card3dNode) {
setParent(sceneView.scene)
renderable = modelRenderable
localScale = modelRenderable.computeScaleVector(targetSize = 1.5f)
currentCard.renderCard()
}
with(sceneView.scene) {
camera.localScale = Vector3(CAMERA_SCALE_WIDTH, CAMERA_SCALE_HEIGHT, CAMERA_FOCAL_LENGTH)
camera.localPosition = Vector3(CAMERA_POSITION_X_AXIS, CAMERA_POSITION_Y_AXIS, CAMERA_POSITION_Z_AXIS)
sunlight?.let {
it.worldPosition = Vector3.back()
it.light = cardSceneSunLight
}
addChild(card3dNode)
}
}
複製代碼
對於虛擬卡,咱們但願實現透明的外觀。爲此,咱們須要建立自定義材料:
material {
"name" : "VirtualCard",
"parameters" : [
{
type : sampler2d,
name : baseColorMap
}
],
requires: [
"uv0"
],
shadingModel: "lit",
blending: "transparent",
transparency : "twoPassesTwoSides",
doubleSided: true,
depthWrite : true
}
fragment {
void material(inout MaterialInputs material) {
prepareMaterial(material);
material.baseColor = texture(materialParams_baseColorMap, getUV0());
}
}
複製代碼
這裏最有趣的部分是 blending
。Transparent
使用 Porter-Duff
的 source over 規則定義材質的輸出與渲染目標的 alpha 合成。
正如您在一次性卡上所注意到的那樣,卡號(或PAN)具備數字變化動畫。對於這個技巧,咱們每秒都改變卡片的漫反射紋理。其中有3個。
一切都不多是完美的,因此咱們面臨一些問題和限制:
material.setTexture
不容許在運行時更改紋理。工做解決方案是建立一個假對象並將此材質複製到真實對象v1.8
SDK以前,沒有辦法設置背景的白色。咱們經過額外的節點和自定義材料解決了這個問題。正如您所注意到的,該卡具備物理基本動畫。Android 提供經過支持庫來實現。
compile "com.android.support:support-dynamic-animation:28.0.0"
複製代碼
在 FlingAnimation 類,您能夠爲對象建立一扔動畫。要構建一個 fling 動畫,請建立一個 FlingAnimation 類的實例,並提供一個對象和要設置動畫的對象屬性。
abstract class CardProperty(name: String) : FloatPropertyCompat<Node>(name)
private val rotationProperty: CardProperty = object : CardProperty("rotation") {
override fun setValue(card: Node, value: Float) {
card.localRotation = getRotationQuaternion(value)
}
override fun getValue(card: Node): Float = card.localRotation.y
}
private var animation: FlingAnimation = FlingAnimation(card3dNode, rotationProperty).apply {
friction = FLING_ANIMATION_FRICTION
minimumVisibleChange = DynamicAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES
}
複製代碼
在 fling 手勢檢測器中,咱們在 onFling
沒有任何更新偵聽器的狀況下運行動畫。只需設定速度便可離開。
class FlingGestureDetector : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
val deltaX = -(distanceX / screenDensity) / CARD_ROTATION_FRICTION
card3dNode.localRotation = getRotationQuaternion(lastDeltaYAxisAngle + deltaX)
return true
}
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
if (Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
val deltaVelocity = (velocityX / screenDensity) / CARD_ROTATION_FRICTION
startAnimation(deltaVelocity)
}
return true
}
}
private fun startAnimation(velocity: Float) {
if (!animation.isRunning) {
animation.setStartVelocity(velocity)
animation.setStartValue(lastDeltaYAxisAngle)
animation.start()
}
}
複製代碼
對於卡片旋轉,咱們使用了一個 localRotation
利用四元數的屬性。Sceneform 有一個靜態方法,它使用軸角表示並經過 axisAngle 和所需的向量計算四元數。在咱們的狀況下 Vector3(0.0f, 1.0f, 0.0f)
。
可是這會在每一個動畫幀中建立冗餘對象,所以咱們須要使用現有的四元數和向量複製此方法:
private val quaternion = Quaternion()
private val rotateVector = Vector3.up()
private fun getRotationQuaternion(deltaYAxisAngle: Float): Quaternion {
lastDeltaYAxisAngle = deltaYAxisAngle
return quaternion.apply {
val arc = toRadians(deltaYAxisAngle)
val axis = sin(arc / 2.0)
x = rotateVector.x * axis
y = rotateVector.y * axis
z = rotateVector.z * axis
w = cos(arc / 2.0)
normalize()
}
}
複製代碼
Sceneform 是一個很是新鮮的庫,但它已經具備普遍的功能:優化渲染,強大的API和小型運行時。全部這些功能幫助咱們快速實現3D,而無需學習OpenGL。
感謝全部參與這一挑戰的人,特別是:
Denis Kovalev, 使人難以置信的UI / UX。 Dmitry Kovalev,他創造了3D模型和紋理。 George Robson,他是Premium團隊的天才全部者。 Ilia Kisliakovskii,咱們的後端英雄。 Mikhail Koltsov和Igor Dudenkov,咱們心愛的iOS人員。