[譯]咱們如何在Revolut中實現3D卡

翻譯說明:

原標題: How we implemented 3D cards in Revolut

原文地址: (https://medium.com/@afeozzz/how-we-implemented

原文做者: Ilnar Karimov

在 Revolut,咱們將客戶體驗置於咱們所作的一切的核心,旨在經過簡單的設計和謹慎的執行帶來愉悅。而後,當咱們介紹卡片訂單流程的更新時,您能夠想象咱們的興奮。在最新版本的 Revolut 應用程序中,您將可以從交互式3D模型中選擇您的卡。html

這對咱們來講是一個有趣的挑戰,由於這是咱們第一次使用基於3D物理的引擎來建立一個功能。咱們認爲結果很是好!java

進入應用程序的卡片訂單部分,您將能夠選擇兩種材料 - 塑料和金屬。從那裏,您將可以選擇一種顏色,以及您是否須要 Visa 或萬事達卡(取決於您所在的國家/地區)。android

讓咱們來看看咱們如何達到這個技術高度,並探索一路上的一些挑戰。git

渲染

從哪裏開始? 首先,咱們嘗試使用 GLSurfaceView,建立咱們本身的渲染器並使用 OpenGL ES 繪製卡片。但這種方法有一些缺點:github

  • 並不是全部移動開發人員都熟悉 OpenGL,這意味着花在培訓和可持續性問題上的時間更多
  • Android已經支持OpenGL ES 3.1,但仍然有一個糟糕的API。結果?大量的樣板,數學和頭髮拉動

因此咱們認爲咱們會找到更好的解決方案。一些搜索引導咱們選擇幾個方面:後端

  • min3d  - 是一個輕量級的3D庫/框架,適用於 Android,使用 Java 和 OpenGL ES,目標是兼容 Android v1.5 / OpenGL ES 1.0及更高版本。此外,min3d有一個更好的API,可是建於2010年,再也不受支持
  • Libgdx  - 是一個徹底Java遊戲開發框架。它提供了許多功能,但對於只想旋轉3D卡的 FinTech 應用程序而言,它太大了
  • Filament  - 是一款基於實時物理的渲染引擎,適用於 Android,iOS,Windows,Linux,macOS和WASM / WebGL。它爲開發人員提供了一組工具和API,以幫助他們輕鬆建立高質量的2D和3D渲染。你可使用它渲染使人難以置信的圖像,咱們強烈建議你使用它。

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());
    }
}
複製代碼

這裏最有趣的部分是 blendingTransparent 使用 Porter-Duff 的 source over 規則定義材質的輸出與渲染目標的 alpha 合成。

正如您在一次性卡上所注意到的那樣,卡號(或PAN)具備數字變化動畫。對於這個技巧,咱們每秒都改變卡片的漫反射紋理。其中有3個。


一切都不多是完美的,因此咱們面臨一些問題和限制:

  • 在具備 CompletableFuture 的模型的適當加載中,Sceneform 要求minSdkVersion≥24。
  • 動態紋理。material.setTexture 不容許在運行時更改紋理。工做解決方案是建立一個假對象並將此材質複製到真實對象
  • v1.8SDK以前,沒有辦法設置背景的白色。咱們經過額外的節點和自定義材料解決了這個問題。

動畫

正如您所注意到的,該卡具備物理基本動畫。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人員。


歡迎關注 Kotlin 中文社區!

中文官網:www.kotlincn.net/

中文官方博客:www.kotliner.cn/

公衆號:Kotlin

知乎專欄:Kotlin

CSDN:Kotlin中文社區

掘金:Kotlin中文社區

簡書:Kotlin中文社區

相關文章
相關標籤/搜索