Link

让我们开始设置构建 MaterialInstance 和我们使用的 GLTF 着色器所需的结构。

将这些结构添加到 vk_types.h

enum class MaterialPass :uint8_t {
    MainColor,
    Transparent,
    Other
};
struct MaterialPipeline {
	VkPipeline pipeline;
	VkPipelineLayout layout;
};

struct MaterialInstance {
    MaterialPipeline* pipeline;
    VkDescriptorSet materialSet;
    MaterialPass passType;
};

这是材质数据所需的结构。MaterialInstance 将保存一个指向其 MaterialPipeline 的原始指针(非拥有),其中包含真正的管线。它也包含一个描述符集。

为了创建这些对象,我们将把逻辑包装到一个结构体中,因为 VulkanEngine 变得太大了,而且我们以后会想要拥有多个材质。

将此添加到 vk_engine.h

struct GLTFMetallic_Roughness {
	MaterialPipeline opaquePipeline;
	MaterialPipeline transparentPipeline;

	VkDescriptorSetLayout materialLayout;

	struct MaterialConstants {
		glm::vec4 colorFactors;
		glm::vec4 metal_rough_factors;
		//padding, we need it anyway for uniform buffers
		glm::vec4 extra[14];
	};

	struct MaterialResources {
		AllocatedImage colorImage;
		VkSampler colorSampler;
		AllocatedImage metalRoughImage;
		VkSampler metalRoughSampler;
		VkBuffer dataBuffer;
		uint32_t dataBufferOffset;
	};

	DescriptorWriter writer;

	void build_pipelines(VulkanEngine* engine);
	void clear_resources(VkDevice device);

	MaterialInstance write_material(VkDevice device, MaterialPass pass, const MaterialResources& resources, DescriptorAllocatorGrowable& descriptorAllocator);
};

我们将保存目前将要使用的 2 个管线,一个用于透明绘制,另一个用于不透明(和 alpha 遮罩)。以及材质的描述符集布局。

我们有一个用于材质常量的结构体,稍后将写入到 uniform 缓冲区中。我们现在需要的参数是 colorFactors,它用于乘以颜色纹理,以及 metal_rough factors,它在 r 和 b 分量上具有金属度和粗糙度参数,外加两个在其他地方使用的参数。

我们还有一堆 vec4 用于填充。在 Vulkan 中,当您想要绑定一个 uniform 缓冲区时,它需要满足对其对齐的最低要求。256 字节是一个很好的默认对齐方式,我们目标的所有 GPU 都满足此要求,因此我们添加这些 vec4 以将结构填充到 256 字节。

当我们创建描述符集时,有一些纹理我们想要绑定,以及包含颜色因子和其他属性的 uniform 缓冲区。我们将把它们保存在 MaterialResources 结构体中,以便轻松地将它们发送到 write_material 函数。

build_pipelines 函数将编译管线。clear_resources 将删除所有内容,而 write_material 是我们创建描述符集并返回一个完全构建的 MaterialInstance 结构体的地方,我们可以在渲染时使用它。

让我们看看这些函数的实现。

void GLTFMetallic_Roughness::build_pipelines(VulkanEngine* engine)
{
	VkShaderModule meshFragShader;
	if (!vkutil::load_shader_module("../../shaders/mesh.frag.spv", engine->_device, &meshFragShader)) {
		fmt::println("Error when building the triangle fragment shader module");
	}

	VkShaderModule meshVertexShader;
	if (!vkutil::load_shader_module("../../shaders/mesh.vert.spv", engine->_device, &meshVertexShader)) {
		fmt::println("Error when building the triangle vertex shader module");
	}

	VkPushConstantRange matrixRange{};
	matrixRange.offset = 0;
	matrixRange.size = sizeof(GPUDrawPushConstants);
	matrixRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

    DescriptorLayoutBuilder layoutBuilder;
    layoutBuilder.add_binding(0,VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
    layoutBuilder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
	layoutBuilder.add_binding(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);

    materialLayout = layoutBuilder.build(engine->_device, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);

	VkDescriptorSetLayout layouts[] = { engine->_gpuSceneDataDescriptorLayout,
        materialLayout };

	VkPipelineLayoutCreateInfo mesh_layout_info = vkinit::pipeline_layout_create_info();
	mesh_layout_info.setLayoutCount = 2;
	mesh_layout_info.pSetLayouts = layouts;
	mesh_layout_info.pPushConstantRanges = &matrixRange;
	mesh_layout_info.pushConstantRangeCount = 1;

	VkPipelineLayout newLayout;
	VK_CHECK(vkCreatePipelineLayout(engine->_device, &mesh_layout_info, nullptr, &newLayout));

    opaquePipeline.layout = newLayout;
    transparentPipeline.layout = newLayout;

	// build the stage-create-info for both vertex and fragment stages. This lets
	// the pipeline know the shader modules per stage
	PipelineBuilder pipelineBuilder;
	pipelineBuilder.set_shaders(meshVertexShader, meshFragShader);
	pipelineBuilder.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
	pipelineBuilder.set_polygon_mode(VK_POLYGON_MODE_FILL);
	pipelineBuilder.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
	pipelineBuilder.set_multisampling_none();
	pipelineBuilder.disable_blending();
	pipelineBuilder.enable_depthtest(true, VK_COMPARE_OP_GREATER_OR_EQUAL);

	//render format
	pipelineBuilder.set_color_attachment_format(engine->_drawImage.imageFormat);
	pipelineBuilder.set_depth_format(engine->_depthImage.imageFormat);

	// use the triangle layout we created
	pipelineBuilder._pipelineLayout = newLayout;

	// finally build the pipeline
    opaquePipeline.pipeline = pipelineBuilder.build_pipeline(engine->_device);

	// create the transparent variant
	pipelineBuilder.enable_blending_additive();

	pipelineBuilder.enable_depthtest(false, VK_COMPARE_OP_GREATER_OR_EQUAL);

	transparentPipeline.pipeline = pipelineBuilder.build_pipeline(engine->_device);
	
	vkDestroyShaderModule(engine->_device, meshFragShader, nullptr);
	vkDestroyShaderModule(engine->_device, meshVertexShader, nullptr);
}

build_pipelines 类似于我们在 VulkanEngine 上拥有的 init_pipelines 函数。我们加载片段和顶点着色器并编译管线。我们也在其中创建管线布局,并且我们还使用相同的管线构建器创建 2 个管线。首先,我们创建不透明管线,然后我们启用混合并创建透明管线。一旦管线清除,我们就可以销毁着色器模块。

您会注意到我们有 2 个新的着色器,这是它们的代码。现在我们将正确渲染材质,我们需要为所有这些创建全新的着色器。

这次我们将在着色器中使用 #includes,因为输入将在片段和顶点着色器上使用。

input_structures.glsl 看起来像这样

layout(set = 0, binding = 0) uniform  SceneData{   

	mat4 view;
	mat4 proj;
	mat4 viewproj;
	vec4 ambientColor;
	vec4 sunlightDirection; //w for sun power
	vec4 sunlightColor;
} sceneData;

layout(set = 1, binding = 0) uniform GLTFMaterialData{   

	vec4 colorFactors;
	vec4 metal_rough_factors;
	
} materialData;

layout(set = 1, binding = 1) uniform sampler2D colorTex;
layout(set = 1, binding = 2) uniform sampler2D metalRoughTex;

我们有一个用于场景数据的 uniform。这将包含视图矩阵和一些额外的参数。这将是全局描述符集。

然后我们为材质的集合 1 绑定了 3 个绑定点。我们有用于材质常量的 uniform,以及 2 个纹理。

mesh.vert 看起来像这样。

#version 450

#extension GL_GOOGLE_include_directive : require
#extension GL_EXT_buffer_reference : require

#include "input_structures.glsl"

layout (location = 0) out vec3 outNormal;
layout (location = 1) out vec3 outColor;
layout (location = 2) out vec2 outUV;

struct Vertex {

	vec3 position;
	float uv_x;
	vec3 normal;
	float uv_y;
	vec4 color;
}; 

layout(buffer_reference, std430) readonly buffer VertexBuffer{ 
	Vertex vertices[];
};

//push constants block
layout( push_constant ) uniform constants
{
	mat4 render_matrix;
	VertexBuffer vertexBuffer;
} PushConstants;

void main() 
{
	Vertex v = PushConstants.vertexBuffer.vertices[gl_VertexIndex];
	
	vec4 position = vec4(v.position, 1.0f);

	gl_Position =  sceneData.viewproj * PushConstants.render_matrix *position;

	outNormal = (PushConstants.render_matrix * vec4(v.normal, 0.f)).xyz;
	outColor = v.color.xyz * materialData.colorFactors.xyz;	
	outUV.x = v.uv_x;
	outUV.y = v.uv_y;
}

我们具有与以前相同的顶点逻辑,但是这次我们在计算位置时将其乘以矩阵。我们还设置了正确的顶点颜色参数和 UV。对于法线,我们仅将顶点法线与渲染矩阵相乘,不包含相机。

mesh.frag 看起来像这样

#version 450

#extension GL_GOOGLE_include_directive : require
#include "input_structures.glsl"

layout (location = 0) in vec3 inNormal;
layout (location = 1) in vec3 inColor;
layout (location = 2) in vec2 inUV;

layout (location = 0) out vec4 outFragColor;

void main() 
{
	float lightValue = max(dot(inNormal, sceneData.sunlightDirection.xyz), 0.1f);

	vec3 color = inColor * texture(colorTex,inUV).xyz;
	vec3 ambient = color *  sceneData.ambientColor.xyz;

	outFragColor = vec4(color * lightValue *  sceneData.sunlightColor.w + ambient ,1.0f);
}

我们正在做一个非常基础的光照着色器。这将使我们能够以更好的方式渲染网格。我们通过将顶点颜色与纹理相乘来计算表面颜色,然后我们做一个简单的光照模型,其中我们有一个单一的阳光和一个环境光。

这是您会在非常老旧的游戏中看到的那种光照,简单的函数,带有 1 个硬编码的光源和非常基本的光照公式乘法。我们稍后将改进这一点,但我们需要一些具有少量光照计算的东西,以便更好地显示材质。

让我们回到 GLTFMetallic_Roughness 并填充 write_material 函数,该函数将创建描述符集并设置参数。

MaterialInstance GLTFMetallic_Roughness::write_material(VkDevice device, MaterialPass pass, const MaterialResources& resources, DescriptorAllocatorGrowable& descriptorAllocator)
{
	MaterialInstance matData;
	matData.passType = pass;
	if (pass == MaterialPass::Transparent) {
		matData.pipeline = &transparentPipeline;
	}
	else {
		matData.pipeline = &opaquePipeline;
	}

	matData.materialSet = descriptorAllocator.allocate(device, materialLayout);


	writer.clear();
	writer.write_buffer(0, resources.dataBuffer, sizeof(MaterialConstants), resources.dataBufferOffset, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
	writer.write_image(1, resources.colorImage.imageView, resources.colorSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
	writer.write_image(2, resources.metalRoughImage.imageView, resources.metalRoughSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);

	writer.update_set(device, matData.materialSet);

	return matData;
}

根据材质通道是什么,我们将为其选择不透明或透明管线,然后我们分配描述符集,并使用来自 MaterialResources 的图像和缓冲区写入它。

让我们创建一个默认材质,我们可以将其用作引擎加载序列的一部分进行测试。

让我们首先将材质结构添加到 VulkanEngine,以及一个 MaterialInstance 结构体用于默认值。

MaterialInstance defaultData;
GLTFMetallic_Roughness metalRoughMaterial;

在 init-pipelines 的末尾,我们调用材质结构上的 build_pipelines 函数来编译它。

void VulkanEngine::init_pipelines()
{
	//rest of initializing functions

    metalRoughMaterial.build_pipelines(this);
}

现在,在 init_default_data() 的末尾,我们使用我们刚刚制作的基本纹理创建默认的 MaterialInstance 结构体。就像我们对场景数据的临时缓冲区所做的那样,我们将分配缓冲区,然后将其放入删除队列,但它将是全局删除队列。在创建默认材质常量缓冲区之后,我们不需要在任何时候访问它。

	GLTFMetallic_Roughness::MaterialResources materialResources;
	//default the material textures
	materialResources.colorImage = _whiteImage;
	materialResources.colorSampler = _defaultSamplerLinear;
	materialResources.metalRoughImage = _whiteImage;
	materialResources.metalRoughSampler = _defaultSamplerLinear;

	//set the uniform buffer for the material data
	AllocatedBuffer materialConstants = create_buffer(sizeof(GLTFMetallic_Roughness::MaterialConstants), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);

	//write the buffer
	GLTFMetallic_Roughness::MaterialConstants* sceneUniformData = (GLTFMetallic_Roughness::MaterialConstants*)materialConstants.allocation->GetMappedData();
	sceneUniformData->colorFactors = glm::vec4{1,1,1,1};
	sceneUniformData->metal_rough_factors = glm::vec4{1,0.5,0,0};

	_mainDeletionQueue.push_function([=, this]() {
		destroy_buffer(materialConstants);
	});

	materialResources.dataBuffer = materialConstants.buffer;
	materialResources.dataBufferOffset = 0;

	defaultData = metalRoughMaterial.write_material(_device,MaterialPass::MainColor,materialResources, globalDescriptorAllocator);

我们将使用默认的白色图像填充 MaterialResources 上材质的参数。然后我们创建一个缓冲区来保存材质颜色,并将其添加到删除队列。然后我们调用 write_material 来创建描述符集并正确初始化该 defaultData 材质。

下一步: 网格和相机