在 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人员。