Vulkan_顶点着色器特效5(关键帧动画)

47 篇文章 9 订阅
20 篇文章 6 订阅

前面几部分分别介绍了 不同软体的例子,虽然采用的数学模型各有不同,但都是通过编程直接实现特定的数学模型以实现软体动画的。这在一般情况下足够用了,但如果想呈现非常复杂的软体动画就很困难了。

有些复杂软体的动画虽然也可以采用数学模型编程实现,但对应的数学模型非常复杂,编程成本很高。本节将给出一种非常简便的实现软体动画的策略——关键帧动画,通过它可以方便地实现游戏中飞行的鸟类、英雄举刀杀敌的动画。
在这里插入图片描述

一、基本原理

关键帧动画的基本思想非常简单,就是给顶点着色器提供动画中每个关键帧对应的各个顶点的位置数据以及融合比例。顶点着色器根据两套位置数据及当前融合的比例融合出一套结果顶点位置数据。只要在绘制每一帧时提供不同的混合比例即可产生想要的动画。

如本节将要给出的展翅飞翔的雄鹰动画中就用到了 3 个关键帧,包含 4 个动画阶段。

  • 第一阶段是对 1、 2 号关键帧中的顶点数据进行融合,即从 1 号关键帧到 2 号关键帧。
  • 第二阶段是对 2、 3 号关键帧中的顶点数据进行融合,即从 2 号关键帧到 3 号关键帧。
  • 第三阶段是对 3、 2 号关键帧中的顶点数据进行融合,即从 3 号关键帧到 2 号关键帧。
  • 第四阶段是对 2、 1 号关键帧中的顶点数据进行融合,即从 2 号关键帧到 1 号关键帧。

上述 4 个阶段不断重复就可以呈现出雄鹰展翅飞翔的动画,每个关键帧的具体顶点位置情况如图下图所示。
在这里插入图片描述

从图中可以看出,最左侧是雄鹰翅膀上扬到最高位置的情况,中间是雄鹰翅膀放平的情况,右侧是雄鹰翅膀下垂到最低位置的情况。
到这里你可能会产生疑问:为什么一定要 3 个关键帧呢?仅保留 1、 3 号关键帧不也能融合出动画吗?确实如此,只保留 1、 3 两个关键帧是可以的,但动画的真实感就会大打折扣。因为仅通过 1、 3 关键帧融合出来的翅膀展平的情况翅膀就会缩短,如图下所示。
在这里插入图片描述
从上图中可以看出使用关键帧动画的一个要领,那就是不重要的中间帧可以通过按比例融合两个关键帧得到,真实感基本不受影响。但关键帧不应该省略而通过其他关键帧融合得到,否则动画的真实感就会变差。

使用基于顶点位置的融合的关键帧动画时有一点需要特别注意,那就是所有关键帧中顶点的数量需要一致,并能够形成一 一对应的关系。

二、开发步骤

2.1 关键帧模型数据加载

本案例中用到的雄鹰的 3 个关键帧采用 3ds Max 设计并导出成 obj 文件(可进个人资源中下载这三个模型文件),因此首先需要将 3 个关键帧的顶点数据加载进应用程序并存放到缓冲中,相关代码如下:

models.example1.loadFromFile(getAssetPath() + "models/hawk01.obj", vertexLayout, 0.5f, vulkanDevice, queue);
models.example2.loadFromFile(getAssetPath() + "models/hawk02.obj", vertexLayout, 0.5f, vulkanDevice, queue);
models.example3.loadFromFile(getAssetPath() + "models/hawk03.obj", vertexLayout, 0.5f, vulkanDevice, queue);

然后,在创建管线布局的时候指定位置:

// Vertex bindings and attributes
const std::vector<VkVertexInputBindingDescription> vertexInputBindings = {
	vks::initializers::vertexInputBindingDescription(0, vertexLayout.stride(), VK_VERTEX_INPUT_RATE_VERTEX),
	vks::initializers::vertexInputBindingDescription(1, vertexLayout.stride(), VK_VERTEX_INPUT_RATE_VERTEX),
	vks::initializers::vertexInputBindingDescription(2, vertexLayout.stride(), VK_VERTEX_INPUT_RATE_VERTEX),
};
const std::vector<VkVertexInputAttributeDescription> vertexInputAttributes = {
	vks::initializers::vertexInputAttributeDescription(0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0),					// Location 0: Position			
	vks::initializers::vertexInputAttributeDescription(0, 1, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 3),		// Location 1: UV
	vks::initializers::vertexInputAttributeDescription(0, 2, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 5),	// Location 2: Color
	vks::initializers::vertexInputAttributeDescription(0, 3, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 8),	// Location 3: Normal
	vks::initializers::vertexInputAttributeDescription(1, 4, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 0),	// Location 4: Position2
	vks::initializers::vertexInputAttributeDescription(2, 5, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 0),	// Location 5: Position3
		};

之后再绑定资源位置的地方分别绑定三个模型的缓冲数据:

vkCmdBindVertexBuffers(drawCmdBuffers[i], 0, 1, &models.example1.vertices.buffer, offsets);
vkCmdBindVertexBuffers(drawCmdBuffers[i], 1, 1, &models.example2.vertices.buffer, offsets);
vkCmdBindVertexBuffers(drawCmdBuffers[i], 2, 1, &models.example3.vertices.buffer, offsets);
vkCmdBindIndexBuffer(drawCmdBuffers[i], models.example.indices.buffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(drawCmdBuffers[i], models.example.indexCount, 1, 0, 0, 0);

上述代码主要是加载 obj 文件中的顶点位置与纹理坐标数据。由于 3 个关键帧中各个对应顶点的纹理坐标是相同的,因此纹理坐标仅保留了一套。但各个关键帧中对应顶点的位置是不同的,因此顶点数据有 3 套。

2.2 顶点着色器

为了在顶点着色器中能够根据比例融合关键帧中的顶点数据,需要将融合的比例传入渲染管线。由于有 3 个关键帧,因此融合比例的取值在 0~2 连续变化。由于将融合比例送入渲染管线的代码非常简单,这里就不再赘述。

接着需要介绍执行顶点融合以产生关键帧动画的顶点着色器,其代码如下。

#version 450

layout (location = 0) in vec4 inPos1;    //顶点位置(来自1号关键帧)
layout (location = 2) in vec3 inColor;
layout (location = 3) in vec3 inNormal;

layout (location = 4) in vec4 inPos2;   //顶点位置(来自2号关键帧)
layout (location = 5) in vec4 inPos3;	//顶点位置(来自3号关键帧)

layout (binding = 0) uniform UBO 
{
	mat4 projection;
	mat4 view;
	mat4 model;
	vec4 lightPos;
	float uBfb;//融合比例
    float padding1;
    float padding2;
	float padding3;
} ubo;

layout (location = 0) out vec3 outNormal;
layout (location = 1) out vec3 outColor;
layout (location = 2) out vec3 outEyePos;
layout (location = 3) out vec3 outLightVec;

void main() 
{
	outNormal = inNormal;
	outColor = inColor;


	vec3 tv;   //融合后的结果顶点      		
   	if(ubo.uBfb<=1.0)//若融合比例小于等于1,则需要执行的是1、2号关键帧的融合
   	{
   		tv=mix(inPos1.xyz,inPos2.xyz,ubo.uBfb);
   	}
   	else//若融合比例大于1,则需要执行的是2、3号关键帧的融合
   	{
   		tv=mix(inPos2.xyz,inPos3.xyz,ubo.uBfb-1.0);
   	}
	vec4 inPos=vec4(tv.xyz,1.0);
	gl_Position = ubo.projection * ubo.view * ubo.model * vec4(inPos.xyz, 1.0);
	outEyePos = vec3(ubo.view * ubo.model * inPos);
	outLightVec = normalize(ubo.lightPos.xyz - outEyePos);

	//针对反射平面的裁剪
	vec4 clipPlane = vec4(0.0, -1.0, 0.0, 1.5);	
	gl_ClipDistance[0] = dot(inPos, clipPlane);	
}

上述顶点着色器是实现关键帧动画的核心,其根据传入的融合比例选择对应的两个关键帧进行融合。需要注意的是,融合时是调用 mix 函数完成的,这是为了提高执行效率。实际开发中有些功能既可以采用函数完成也可以自己编程完成,强烈建议直接调用函数完成。这是因为系统的函数在大部分情况下比自己开发的相同功能的代码片段性能优异。
执行可见开头部分gif效果。
附三个关键帧动画加载的图片:
关键帧1:
在这里插入图片描述
关键帧2:

在这里插入图片描述
关键帧3:
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值