[译]咱们如何在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中文社区

相关文章
相关标签/搜索