Вступ
Я читаю онлайн книгу про WebGPU. У цій серії статей я буду розглядати цю книгу та реалізовувати уроки в більш структурованому підході з використанням класів TypeScript, і в кінцевому підсумку ми побудуємо три типи рендерерів WebGPU: Gaussian Splatting, Ray tracing та Rasterization.
У цій статті ми поговоримо про Depth Peeling — техніку рендерингу, що використовується для точного відображення перекритих прозорих об'єктів шляхом "зняття" шарів глибини сцени. В кожному проході вона захоплює найближчі видимі прозорі фрагменти, змішує їх, а потім переходить до наступного шару ззаду, поки всі шари не будуть відрендерені. Це забезпечує правильне змішування та уникнення артефактів через некоректне сортування глибини, хоча така техніка може бути витратною через багаторазові проходи рендерингу.
Наступне посилання — це коміт у моєму репозиторії на GitHub, що відповідає коду, який ми будемо розглядати.
Depth Peeling
Ми будемо рендерити напівпрозору чайничок, знімаючи та відображаючи кілька фрагментів, щоб побудувати наше фінальне зображення.
Перший Прохід:
- Рендеримо сцену та захоплюємо найближчу поверхню чайника (наприклад, носик або кришку).
- Зберігаємо її глибину та змішуємо її колір з текстурою призначення.
Другий Прохід:
- Захоплюємо наступний видимий шар (наприклад, корпус чайника за носиком), використовуючи буфер глибини з попереднього проходу для вибору фрагментів, які будуть показані.
- Змішуємо його з попереднім шаром.
Повторюємо:
- Продовжуємо рендеринг та змішування глибших шарів, поки не досягнемо максимального числа проходів (в нашому випадку 6).
Наш перший шейдер — це наш Blend шейдер, він буде вибирати зразок з нашої текстури для малювання на текстуру виходу.
Наш пайплайн змішування завантажить задану текстуру призначення та малюватиме на ній, поступово накопичуючи кольори на нашій текстурі і зрештою малюючи результат на екрані.
struct VertexOutput {
@builtin(position) clip_position: vec4,
@location(0) tex_coords: vec2
};
@vertex
fn vs_main(
@location(0) inPos: vec4
) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4(inPos.xy, 0.0, 1.0);
out.tex_coords = inPos.zw;
return out;
}
// Fragment shader
@group(0) @binding(0)
var t_src: texture_2d;
@group(0) @binding(1)
var s: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4 {
var color:vec4 = textureSample(t_src, s, in.tex_coords);
return color;
}
Наш шейдер OBJ буде дуже схожий на ті, що ми маємо в інших шейдерах OBJ.
@group(0) @binding(0)
var modelView: mat4x4;
@group(0) @binding(1)
var projection: mat4x4;
@group(0) @binding(2)
var normalMatrix: mat4x4;
@group(0) @binding(3)
var lightDirection: vec3;
@group(0) @binding(4)
var viewDirection: vec3;
@group(1) @binding(0)
var offset: vec3;
@group(1) @binding(1)
var ambientColor:vec4;// = vec4(0.15, 0.10, 0.10, 1.0);
@group(1) @binding(2)
var diffuseColor:vec4;// = vec4(0.55, 0.55, 0.55, 1.0);
@group(1) @binding(3)
var specularColor:vec4;// = vec4(1.0, 1.0, 1.0, 1.0);
@group(1) @binding(4)
var shininess:f32;// = 20.0;
const diffuseConstant:f32 = 1.0;
const specularConstant:f32 = 1.0;
const ambientConstant: f32 = 1.0;
fn specular(lightDir:vec3, viewDir:vec3, normal:vec3, specularColor:vec3,
shininess:f32) -> vec3 {
let reflectDir:vec3 = reflect(-lightDir, normal);
let specDot:f32 = max(dot(reflectDir, viewDir), 0.0);
return pow(specDot, shininess) * specularColor;
}
fn diffuse(lightDir:vec3, normal:vec3, diffuseColor:vec3) -> vec3{
return max(dot(lightDir, normal), 0.0) * diffuseColor;
}
struct VertexOutput {
@builtin(position) clip_position: vec4,
@location(0) viewDir: vec3,
@location(1) normal: vec3,
@location(2) lightDir: vec3,
@location(3) inPos: vec4,
};
@vertex
fn vs_main(
@location(0) inPos: vec3,
@location(1) inNormal: vec3
) -> VertexOutput {
var out: VertexOutput;
out.viewDir = normalize((normalMatrix * vec4(-viewDirection, 0.0)).xyz);
out.lightDir = normalize((normalMatrix * vec4(-lightDirection, 0.0)).xyz);
out.normal = normalize(normalMatrix * vec4(inNormal, 0.0)).xyz;
var wldLoc:vec4 = modelView * vec4(inPos+offset, 1.0);
out.clip_position = projection * wldLoc;
out.inPos = projection * wldLoc;
return out;
}
@group(2) @binding(0)
var t_depth: texture_depth_2d;
@group(2) @binding(1)
var s_depth: sampler_comparison;
@fragment
fn fs_main(in: VertexOutput, @builtin(front_facing) face: bool) -> @location(0) vec4 {
var uv:vec2 = 0.5*(in.inPos.xy/in.inPos.w + vec2(1.0,1.0));
var visibility:f32 = textureSampleCompare(
t_depth, s_depth,
vec2(uv.x, 1.0-uv.y), in.clip_position.z - 0.0001
);
if (visibility < 0.5) {
discard;
}
var lightDir:vec3 = normalize(in.lightDir);
var n:vec3 = normalize(in.normal);
var color:vec3 = diffuseColor.rgb;
if (!face) {
n = normalize(-in.lightDir);
}
var viewDir: vec3 = in.viewDir;
var radiance:vec3 = ambientColor.rgb * ambientConstant +
diffuse(-lightDir, n, color)* diffuseConstant +
specular(-lightDir, viewDir, n, specularColor.rgb, shininess) * specularConstant;
return vec4(radiance * diffuseColor.w, diffuseColor.w);
}
Наше значення видимості використовується для перевірки, чи знаходиться фрагмент попереду значень, збережених у нашому буфері глибини з попереднього проходу, мінус невеликий зсув, що дає нам тільки ті фрагменти, які потрапляють на наступний шар.
Ми проходимо стандартні етапи перевірки напрямку поверхні та отримуємо значення радіації для освітлення.
Одна ключова різниця полягає в тому, що ми передмножуємо вихідний колір RGB на альфа-канал.
Ми передмножуємо альфу, щоб зробити змішування математично коректним і ефективним, особливо для напівпрозорих об'єктів.
Це дозволяє уникнути артефактів, гарантуючи, що канали кольору вже масштабовані їх прозорістю, спрощуючи інтерполяцію та композитинг.
## Класи для Depth Peeling
Ми матимемо клас для керування інфраструктурою пайплайна, клас для рендерингу чайника та класи для виконання проміжних змішувань і фінального "змішування", яке малює на екрані.
## Пайплайн
Наш процес пайплайна досить складний, і він має використовуватися чайником OBJ, тому ми інкапсулюємо його в окремий клас.
export class Pipeline {
private _uniformBindGroupLayoutPeeling: GPUBindGroupLayout;
private _uniformBindGroupLayoutObject: GPUBindGroupLayout;
private _uniformBindGroupGlobal: GPUBindGroup;
private _renderPipeline: GPURenderPipeline;
private _sampler: GPUSampler;
private _uniformBindGroupPeeling0?: GPUBindGroup;
private _uniformBindGroupPeeling1?: GPUBindGroup;
public get uniformBindGroupLayoutObject(): GPUBindGroupLayout {
return this._uniformBindGroupLayoutObject;
}
public get uniformBindGroupGlobal(): GPUBindGroup {
return this._uniformBindGroupGlobal;
}
public get uniformBindGroupPeeling0(): GPUBindGroup | undefined {
return this._uniformBindGroupPeeling0;
}
public get uniformBindGroupPeeling1(): GPUBindGroup | undefined {
return this._uniformBindGroupPeeling1;
}
public get renderPipeline(): GPURenderPipeline {
return this._renderPipeline;
}
public static async init(device: GPUDevice, modelViewMatrixUniformBuffer: GPUBuffer,
projectionMatrixUniformBuffer: GPUBuffer, normalMatrixUniformBuffer: GPUBuffer,
viewDirectionUniformBuffer: GPUBuffer, lightDirectionUniformBuffer: GPUBuffer, shaderCode: string): Promise {
const shaderModule: GPUShaderModule = device.createShaderModule({ code: shaderCode });
const sampler: GPUSampler = device.createSampler({
addressModeU: 'clamp-to-edge',
addressModeV: 'clamp-to-edge',
magFilter: 'nearest',
minFilter: 'nearest',
mipmapFilter: 'nearest',
compare: 'greater'
});
const uniformBindGroupLayoutGlobal: GPUBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: {}
},
{
binding: 1,
visibility: GPUShaderStage.VERTEX,
buffer: {}
},
{
binding: 2,
visibility: GPUShaderStage.VERTEX,
buffer: {}
},
{
binding: 3,
visibility: GPUShaderStage.VERTEX,
buffer: {}
},
{
binding: 4,
visibility: GPUShaderStage.VERTEX,
buffer: {}
}
]
});
const uniformBindGroupLayoutObject: GPUBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: {}
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
buffer: {}
},
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
buffer: {}
},
{
binding: 3,
visibility: GPUShaderStage.FRAGMENT,
buffer: {}
},
{
binding: 4,
visibility: GPUShaderStage.FRAGMENT,
buffer: {}
}
]
});
const uniformBindGroupLayoutPeeling: GPUBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
texture: {
sampleType: "depth"
}
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
sampler: {
type: 'comparison',
},
}
]
});
const uniformBindGroupGlobal: GPUBindGroup = device.createBindGroup({
layout: uniformBindGroupLayoutGlobal,
entries: [
{
binding: 0,
resource: {
buffer: modelViewMatrixUniformBuffer
}
},
{
binding: 1,
resource: {
buffer: projectionMatrixUniformBuffer
}
},
{
binding: 2,
resource: {
buffer: normalMatrixUniformBuffer
}
},
{
binding: 3,
resource: {
buffer: lightDirectionUniformBuffer
}
},
{
binding: 4,
resource: {
buffer: viewDirectionUniformBuffer
}
}
]
});
const positionAttribDesc: GPUVertexAttribute = {
shaderLocation: 0,
offset: 0,
format: 'float32x3'
};
const positionBufferLayoutDesc: GPUVertexBufferLayout = {
attributes: [positionAttribDesc],
arrayStride: Float32Array.BYTESPERELEMENT * 3,
stepMode: 'vertex'
};
const normalAttribDesc: GPUVertexAttribute = {
shaderLocation: 1,
offset: 0,
format: 'float32x3'
};
const normalBufferLayoutDesc: GPUVertexBufferLayout = {
attributes: [normalAttribDesc],
arrayStride: Float32Array.BYTESPERELEMENT * 3,
stepMode: 'vertex'
};
const layout: GPUPipelineLayout = device.createPipelineLayout(
{
bindGroupLayouts: [uniformBindGroupLayoutGlobal, uniformBindGroupLayoutObject, uniformBindGroupLayoutPeeling]
}
);
const colorState: GPUColorTargetState = {
format: 'bgra8unorm'
};
const pipelineDesc: GPURenderPipelineDescriptor = {
layout: layout,
vertex: {
module: shaderModule,
entryPoint: 'vsmain',
buffers: [positionBufferLayoutDesc, normalBufferLayoutDesc]
},
fragment: {
module: shaderModule,
entryPoint: 'fsmain',
targets: [colorState]
},
primitive: {
topology: 'triangle-list',
frontFace: 'ccw',
cullMode: 'none'
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth32float'
}
}
const pipeline: GPURenderPipeline = device.createRenderPipeline(pipelineDesc);
return new Pipeline(uniformBindGroupLayoutPeeling, uniformBindGroupLayoutObject, uniformBindGroupGlobal, sampler, pipeline);
}
public updateDepthPeelingUniformGroup(device: GPUDevice, depthTexture0: GPUTexture, depthTexture1: GPUTexture) {
this.uniformBindGroupPeeling0 = device.createBindGroup({
layout:this.uniformBindGroupLayoutPeeling,
entries: [
{
binding: 0,
resource: depthTexture0.createView()
},
{
binding: 1,
resource:
this._sampler
}
]
});
this.uniformBindGroupPeeling1 = device.createBindGroup({
layout:this.uniformBindGroupLayoutPeeling,
entries: [
{
binding: 0,
resource: depthTexture1.createView()
},
{
binding: 1,
resource:
this._sampler
}
]
});
}
private constructor(uniformBindGroupLayoutPeeling: GPUBindGroupLayout, uniformBindGroupLayoutObject: GPUBindGroupLayout, uniformBindGroupGlobal: GPUBindGroup, sampler: GPUSampler, renderPipeline: GPURenderPipeline) {
this.uniformBindGroupLayoutPeeling = uniformBindGroupLayoutPeeling;
this.uniformBindGroupGlobal = uniformBindGroupGlobal;
this.uniformBindGroupLayoutObject = uniformBindGroupLayoutObject;
this.sampler = sampler;
this._renderPipeline = renderPipeline;
}
}
```
Ми будуємо пайплайн, який має 3 групи зв'язування: одну для глобального освітлення та проекцій, одну для кольорів нашого OBJ та одну для зберігання значень текстури глибини з попереднього проходу рендерингу.
Ми відкриємо функцію, щоб дозволити нашому застосунку оновлювати текстури глибини, між якими ми будемо чергувати під час рендерингу.
Чайник
Наш чайник завантажить деякі дані вершин з нашого OBJ чайника та призначить кольори для використання в шейдері фрагментів чайника.
Нарешті, ми налаштуємо наш пайплайн рендерингу.
export class Teapot {
private _positionBuffer: GPUBuffer;
private _normalBuffer: GPUBuffer;
private _uniformBindGroup: GPUBindGroup;
private _indexBuffer?: GPUBuffer;
private _indexSize?: number;
public static async init(device: GPUDevice, pipeline: Pipeline): Promise {
const objResponse = await fetch("./objs/teapot.obj");
const objBlob = await objResponse.blob();
const objText = await objBlob.text();
const objDataExtractor = new ObjDataExtractor(objText);
const positions = objDataExtractor.vertexPositions;
const positionBuffer = createGPUBuffer(device, positions, GPUBufferUsage.VERTEX);
const normals = objDataExtractor.normals;
const normalBuffer = createGPUBuffer(device, normals, GPUBufferUsage.VERTEX);
const indices = objDataExtractor.indices;
const indexBuffer = createGPUBuffer(device, indices, GPUBufferUsage.INDEX);
const indexSize = indices.length;
const ambientUniformBuffer = createGPUBuffer(device, new Float32Array([0.05, 0.01, 0.01, 1.0]), GPUBufferUsage.UNIFORM);
const diffuseUniformBuffer = createGPUBuffer(device, new Float32Array([0.85, 0.05, 0.05, 0.5]), GPUBufferUsage.UNIFORM);
const specularUniformBuffer = createGPUBuffer(device, new Float32Array([1.0, 1.0, 1.0, 1.0]), GPUBufferUsage.UNIFORM);
const shininessUniformBuffer = createGPUBuffer(device, new Float32Array([80.0]), GPUBufferUsage.UNIFORM);
const offsetUniformBuffer = createGPUBuffer(device, new Float32Array([0.0, 0.0, 0.0]), GPUBufferUsage.UNIFORM);
const uniformBindGroup = device.createBindGroup({
layout: pipeline.uniformBindGroupLayoutObject,
entries: [
{
binding: 0,
resource: {
buffer: offsetUniformBuffer
}
},
{
binding: 1,
resource: {
buffer: ambientUniformBuffer
}
},
{
binding: 2,
resource: {
buffer: diffuseUniformBuffer
}
},
{
binding: 3,
resource: {
buffer: specularUniformBuffer
}
},
{
binding: 4,
resource: {
buffer: shininessUniformBuffer
}
}
]
});
return new Teapot(positionBuffer, normalBuffer, uniformBindGroup, indexBuffer, indexSize);
}
public encodeRenderPass(renderPassEncoder: GPURenderPassEncoder, pipeline: Pipeline, peelingTextureIndex: number) {
renderPassEncoder.setPipeline(pipeline.renderPipeline);
renderPassEncoder.setBindGroup(0, pipeline.uniformBindGroupGlobal);
renderPassEncoder.setBindGroup(1, this._uniformBindGroup);
if (peelingTextureIndex % 2 == 0) {
renderPassEncoder.setBindGroup(2, pipeline.uniformBindGroupPeeling0);
} else {
renderPassEncoder.setBindGroup(2, pipeline.uniformBindGroupPeeling1);
}
renderPassEncoder.setVertexBuffer(0, this._positionBuffer);
renderPassEncoder.setVertexBuffer(1, this._normalBuffer);
renderPassEncoder.setIndexBuffer(this._indexBuffer!, 'uint16');
renderPassEncoder.drawIndexed(this._indexSize!);
}
private constructor(positionBuffer: GPUBuffer, normalBuffer: GPUBuffer, uniformBindGroup: GPUBindGroup, indexBuffer: GPUBuffer, indexSize: number) {
this._positionBuffer = positionBuffer;
this._normalBuffer = normalBuffer;
this._uniformBindGroup = uniformBindGroup;
this._indexBuffer = indexBuffer;
this._indexSize = indexSize;
}
}
Тепер наша звична функція encodeRenderPass отримує додатковий параметр peelingTextureIndex, який вказує, з якої текстури глибини ми маємо читати.
В нашому пайплайні ми будемо по черзі читати з однієї текстури глибини та писати в іншу, а потім міняти їх місцями.
Змішування
Цей клас Blend відповідає за отримання текстури та малювання її на нашу вихідну текстуру.
export class Blend {
private _uniformBindGroupLayout: GPUBindGroupLayout;
private _sampler: GPUSampler;
private _positionBuffer: GPUBuffer;
private _pipeline: GPURenderPipeline;
private _uniformBindGroup?: GPUBindGroup;
public static async init(device: GPUDevice, shaderCode: string): Promise {
const shaderModule: GPUShaderModule = device.createShaderModule({ code: shaderCode });
const sampler: GPUSampler = device.createSampler({
addressModeU: 'clamp-to-edge',
addressModeV: 'clamp-to-edge',
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear'
});
const uniformBindGroupLayout: GPUBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
texture: {}
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
sampler: {}
}
]
});
const positions: Float32Array = new Float32Array([
-1, -1, 0, 1,
1, -1, 1, 1,
-1, 1, 0, 0,
1, 1, 1, 0
]);
const positionBuffer: GPUBuffer = createGPUBuffer(device, positions, GPUBufferUsage.VERTEX);
const positionAttribDesc: GPUVertexAttribute = {
shaderLocation: 0,
offset: 0,
format: 'float32x4'
};
const positionBufferLayoutDesc: GPUVertexBufferLayout = {
attributes: [positionAttribDesc],
arrayStride: Float32Array.BYTES_PER_ELEMENT * 4,
stepMode: 'vertex'
};
const layout: GPUPipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [uniformBindGroupLayout]
});
const colorState: GPUColorTargetState = {
format: 'bgra8unorm',
blend: {
color: {
operation: "add",
srcFactor: 'dst-alpha',
dstFactor: 'one',
},
alpha: {
operation: "add",
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha',
}
}
};
const pipelineDesc: GPURenderPipelineDescriptor = {
layout: layout,
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [positionBufferLayoutDesc]
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [colorState]
},
primitive: {
topology: 'triangle-strip',
frontFace: 'ccw',
cullMode: 'none'
}
}
const pipeline = device.createRenderPipeline(pipelineDesc);
return new Blend(uniformBindGroupLayout, sampler, positionBuffer, pipeline);
}
public updateTexture(device: GPUDevice, srcTexture: GPUTexture) {
this._uniformBindGroup = device.createBindGroup({
layout: this._uniformBindGroupLayout,
entries: [
{
binding: 0,
resource: srcTexture.createView()
},
{
binding: 1,
resource: this._sampler
}
]
})
}
public encodeRenderPass(renderPassEncoder: GPURenderPassEncoder) {
renderPassEncoder.setPipeline(this._pipeline);
renderPassEncoder.setBindGroup(0, this._uniformBindGroup);
renderPassEncoder.setVertexBuffer(0, this._positionBuffer);
renderPassEncoder.draw(4, 1);
}
constructor(uniformBindGroupLayout: GPUBindGroupLayout, sampler: GPUSampler, positionBuffer: GPUBuffer, pipeline: GPURenderPipeline) {
this._sampler = sampler;
this._uniformBindGroupLayout = uniformBindGroupLayout;
this._positionBuffer = positionBuffer;
this._pipeline = pipeline;
}
}
Коли ми малюємо задану текстуру на нашу цільову текстуру, ми використовуємо спеціальну конфігурацію змішування.
Коли ми малюємо колір, колір призначення буде зважений на 1, а колір джерела буде зважений на поточний альфа-канал призначення.
outputColor=(srcColor×dstAlpha)+(dstColor×1)
Коли ми вибираємо фінальний альфа-канал, ми будемо зважувати наше призначення на 1 - альфа джерела.
outputAlpha=(srcAlpha×0)+(dstAlpha×(1−srcAlpha))
= dstAlpha - dstAlpha*srcAlpha
Змішування кольору дозволяє кольору джерела змішуватися більше з кольором призначення, якщо призначення більш прозоре, в той час як змішування альфи робить так, що чим прозоріше джерело, тим менше воно впливає на колір і прозорість фінального результату.
## Останнє
Останній клас змішування малюватиме нашу напівпрозору текстуру на фінальному фоні.
export class Final {
private _uniformBindGroupLayout: GPUBindGroupLayout;
private _sampler: GPUSampler;
private _positionBuffer: GPUBuffer;
private _pipeline: GPURenderPipeline;
private _uniformBindGroup?: GPUBindGroup;
public static async init(device: GPUDevice, shaderCode: string): Promise {
const shaderModule: GPUShaderModule = device.createShaderModule({ code: shaderCode });
const sampler: GPUSampler = device.createSampler({
addressModeU: 'clamp-to-edge',
addressModeV: 'clamp-to-edge',
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear'
});
const uniformBindGroupLayout: GPUBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
texture: {}
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
sampler: {}
}
]
});
const positions: Float32Array = new Float32Array([
-1, -1, 0, 1,
1, -1, 1, 1,
-1, 1, 0, 0,
1, 1, 1, 0
]);
const positionBuffer: GPUBuffer = createGPUBuffer(device, positions, GPUBufferUsage.VERTEX);
const positionAttribDesc: GPUVertexAttribute = {
shaderLocation: 0,
offset: 0,
format: 'float32x4'
};
const positionBufferLayoutDesc: GPUVertexBufferLayout = {
attributes: [positionAttribDesc],
arrayStride: Float32Array.BYTESPERELEMENT * 4,
stepMode: 'vertex'
};
const layout: GPUPipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [uniformBindGroupLayout]
});
const colorState: GPUColorTargetState = {
format: 'bgra8unorm',
blend: {
color: {
operation: "add",
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
},
alpha: {
operation: "add",
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
}
}
};
const pipelineDesc: GPURenderPipelineDescriptor = {
layout: layout,
vertex: {
module: shaderModule,
entryPoint: 'vsmain',
buffers: [positionBufferLayoutDesc]
},
fragment: {
module: shaderModule,
entryPoint: 'fsmain',
targets: [colorState]
},
primitive: {
topology: 'triangle-strip',
frontFace: 'ccw',
cullMode: 'none'
}
}
const pipeline = device.createRenderPipeline(pipelineDesc);
return new Final(uniformBindGroupLayout, sampler, positionBuffer, pipeline);
}
public updateTexture(device: GPUDevice, dstTexture: GPUTexture) {
this.uniformBindGroup = device.createBindGroup({
layout: this.uniformBindGroupLayout,
entries: [
{
binding: 0,
resource: dstTexture.createView()
},
{
binding: 1,
resource: this._sampler
}
]
})
}
public encodeRenderPass(renderPassEncoder: GPURenderPassEncoder) {
renderPassEncoder.setPipeline(this.pipeline);
renderPassEncoder.setBindGroup(0, this.uniformBindGroup);
renderPassEncoder.setVertexBuffer(0, this._positionBuffer);
renderPassEncoder.draw(4, 1);
}
constructor(uniformBindGroupLayout: GPUBindGroupLayout, sampler: GPUSampler, positionBuffer: GPUBuffer, pipeline: GPURenderPipeline) {
this.sampler = sampler;
this.uniformBindGroupLayout = uniformBindGroupLayout;
this.positionBuffer = positionBuffer;
this.pipeline = pipeline;
}
}
```
Наш колір буде повністю походити з нашої текстури, якщо вона повністю непрозора (тобто)
alpha = 1).
Якщо ні, частина кольору буде братися з текстури фону — саме 1-альфа частина кольору.
Наша альфа буде повністю непрозорою, якщо dst_alpha дорівнює 1, оскільки рівняння таке:
final_alpha = src_alpha + (1-src_alpha)*dst_alpha
Якщо у нас був би більш прозорий фон (наприклад, альфа = 0.9), ми отримуємо:
final_alpha = 0.1 * src_alpha + 0.9
Таким чином, ми можемо отримати фінальну напівпрозору текстуру, якщо як змішувана текстура, так і текстура фону мають певну прозорість.
Рендеринг
Наш пайплайн рендерингу налаштує наш пайплайн, об'єкти чайника, змішування та фінальне змішування і створить дві текстури глибини: фінальну destinationTexture та colorTextureForCleanup для використання під час процесу змішування.
Наш об'єкт змішування буде використовувати текстуру colorTextureForCleanup, тоді як наш об'єкт фінального змішування використовуватиме текстуру призначення.
const renderTransparencyExample = async () => {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter!.requestDevice();
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const context = canvas.getContext("webgpu");
const canvasConfig: GPUCanvasConfiguration = {
device: device!,
format: navigator.gpu.getPreferredCanvasFormat() as GPUTextureFormat,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
alphaMode: "opaque",
}
context!.configure(canvasConfig);
let angle = 0.0;
const arcball = new Arcball(5.0);
const modelViewMatrix = arcball.getMatrices();
const modelViewMatrixUniformBuffer = createGPUBuffer(device!, new Float32Array(modelViewMatrix), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
const viewDir = glMatrix.vec3.fromValues(-10.0, -10.0, -10.0);
const viewDirectionUniformBuffer = createGPUBuffer(device!, new Float32Array(viewDir), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
const lightDirectionBuffer = createGPUBuffer(device!, new Float32Array(viewDir), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
const modelViewMatrixInverse = glMatrix.mat4.invert(glMatrix.mat4.create(), modelViewMatrix)!;
const normalMatrix = glMatrix.mat4.transpose(glMatrix.mat4.create(), modelViewMatrixInverse);
const normalMatrixUniformBuffer = createGPUBuffer(device!, new Float32Array(normalMatrix), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
const projectionMatrix = glMatrix.mat4.perspective(glMatrix.mat4.create(), 1.4, canvas.width / canvas.height, 0.1, 1000.0);
const projectionMatrixUnifromBuffer = createGPUBuffer(device!, new Float32Array(projectionMatrix), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
const pipeline = await Pipeline.init(device!, modelViewMatrixUniformBuffer, projectionMatrixUnifromBuffer, normalMatrixUniformBuffer, viewDirectionUniformBuffer, lightDirectionBuffer, transparencyObjModelWgsl);
const teapot = await TransparentTeapot.init(device!, pipeline);
const final = await Final.init(device!, finalBlendShader);
const blend = await Blend.init(device!, blendShader);
let depthTexture0: GPUTexture | null = null;
let depthStencilAttachment0: GPURenderPassDepthStencilAttachment | undefined = undefined;
let depthTexture1: GPUTexture | null = null;
let depthStencilAttachment1: GPURenderPassDepthStencilAttachment | undefined = undefined;
let dstTexture: GPUTexture | null = null;
let colorTextureForCleanup: GPUTexture | null = null;
async function render() {
const devicePixelRatio = window.devicePixelRatio || 1;
let currentCanvasWidth = canvas.clientWidth * devicePixelRatio;
let currentCanvasHeight = canvas.clientHeight * devicePixelRatio;
let projectionMatrixUniformBufferUpdate = null;
let colorTextureForDebugging: GPUTexture | null = null;
if (currentCanvasWidth != canvas.width || currentCanvasHeight != canvas.height || colorTextureForDebugging == null || dstTexture == null || depthTexture0 == null || depthTexture1 == null) {
canvas.width = currentCanvasWidth;
canvas.height = currentCanvasHeight;
if (depthTexture0 !== null) {
depthTexture0.destroy();
}
if (depthTexture1 !== null) {
depthTexture1.destroy();
}
const depthTextureDesc: GPUTextureDescriptor = {
size: [canvas.width, canvas.height, 1],
dimension: '2d',
format: 'depth32float',
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING
};
depthTexture0 = device!.createTexture(depthTextureDesc);
depthTexture0.label = "DEPTH_0"
depthTexture1 = device!.createTexture(depthTextureDesc);
depthTexture1.label = "DEPTH_1"
pipeline.updateDepthPeelingUniformGroup(device!, depthTexture0, depthTexture1);
depthStencilAttachment0 = {
view: depthTexture1.createView(),
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store'
};
depthStencilAttachment1 = {
view: depthTexture0.createView(),
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store'
};
let projectionMatrix = glMatrix.mat4.perspective(glMatrix.mat4.create(),
1.4, canvas.width / canvas.height, 0.1, 1000.0);
projectionMatrixUniformBufferUpdate = createGPUBuffer(device!, new Float32Array(projectionMatrix), GPUBufferUsage.COPY_SRC);
const colorTextureForDstDesc: GPUTextureDescriptor = {
size: [canvas.width, canvas.height, 1],
dimension: '2d',
format: 'bgra8unorm',
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
};
if (colorTextureForCleanup !== null) {
colorTextureForCleanup.destroy();
}
colorTextureForDebugging = device!.createTexture(colorTextureForDstDesc);
colorTextureForDebugging.label ="DEBUG_TEXTURE";
colorTextureForCleanup = colorTextureForDebugging;
if (dstTexture !== null) {
dstTexture.destroy();
}
dstTexture = device!.createTexture(colorTextureForDstDesc);
dstTexture.label ="DEST_TEXTURE";
blend.updateTexture(device!, colorTextureForDebugging);
final.updateTexture(device!, dstTexture);
}
const modelViewMatrix = arcball.getMatrices();
const modelViewMatrixUniformBufferUpdate = createGPUBuffer(device!, new Float32Array(modelViewMatrix), GPUBufferUsage.COPY_SRC);
const modelViewMatrixInverse = glMatrix.mat4.invert(glMatrix.mat4.create(), modelViewMatrix);
const normalMatrix = glMatrix.mat4.transpose(glMatrix.mat4.create(), modelViewMatrixInverse);
const normalMatrixUniformBufferUpdate = createGPUBuffer(device!, new Float32Array(normalMatrix), GPUBufferUsage.COPY_SRC);
const viewDir = glMatrix.vec3.fromValues(-arcball.forward[0], -arcball.forward[1], -arcball.forward[2]);
const viewDirectionUniformBufferUpdate = createGPUBuffer(device!, new Float32Array(viewDir), GPUBufferUsage.COPY_SRC);
const lightDir = glMatrix.vec3.fromValues(Math.cos(angle) * 8.0, Math.sin(angle) * 8.0, 10);
const lightDirectionBufferUpdate = createGPUBuffer(device!, new Float32Array(lightDir), GPUBufferUsage.COPY_SRC);
const colorTexture = context!.getCurrentTexture();
const colorTextureView = colorTexture.createView();
const colorAttachment0: GPURenderPassColorAttachment = {
view: colorTextureForDebugging!.createView(),
clearValue: { r: 0, g: 0, b: 0, a: 0 },
loadOp: 'clear',
storeOp: 'store'
};
const colorAttachment1: GPURenderPassColorAttachment = {
view: colorTextureForDebugging!.createView(),
clearValue: { r: 0, g: 0, b: 0, a: 0 },
loadOp: 'clear',
storeOp: 'store'
};
const cleanUpColorAttachment: GPURenderPassColorAttachment ={
view: dstTexture!.createView(),
clearValue: {r: 0, g: 0, b: 0, a: 1},
loadOp: 'clear',
storeOp: 'store'
}
const blendColorAttachment: GPURenderPassColorAttachment ={
view: dstTexture!.createView(),
clearValue: {r: 0, g: 0, b: 0, a: 0},
loadOp: 'load',
storeOp: 'store'
}
const finalColorAttachment: GPURenderPassColorAttachment ={
view: colorTextureView,
clearValue: {r: 0, g: 0, b: 0, a: 1},
loadOp: 'load',
storeOp: 'store'
}
const renderPassCleanupDesc: GPURenderPassDescriptor = {
colorAttachments: [cleanUpColorAttachment]
};
const renderPassDesc0: GPURenderPassDescriptor = {
colorAttachments: [colorAttachment0],
depthStencilAttachment: depthStencilAttachment0
}
const renderPassDesc1: GPURenderPassDescriptor = {
colorAttachments: [colorAttachment1],
depthStencilAttachment: depthStencilAttachment1
}
const renderPassBlend: GPURenderPassDescriptor = {
colorAttachments: [blendColorAttachment]
}
const renderPassFinal: GPURenderPassDescriptor = {
colorAttachments: [finalColorAttachment]
}
const commandEncoder = device!.createCommandEncoder();
if (projectionMatrixUniformBufferUpdate != null) {
commandEncoder.copyBufferToBuffer(projectionMatrixUniformBufferUpdate, 0, projectionMatrixUnifromBuffer, 0, 16 * Float32Array.BYTES_PER_ELEMENT);
}
commandEncoder.copyBufferToBuffer(modelViewMatrixUniformBufferUpdate, 0, modelViewMatrixUniformBuffer, 0, 16 * Float32Array.BYTES_PER_ELEMENT);
commandEncoder.copyBufferToBuffer(normalMatrixUniformBufferUpdate, 0, normalMatrixUniformBuffer, 0, 16 * Float32Array.BYTES_PER_ELEMENT);
commandEncoder.copyBufferToBuffer(viewDirectionUniformBufferUpdate, 0, viewDirectionUniformBuffer, 0, 3 * Float32Array.BYTES_PER_ELEMENT);
commandEncoder.copyBufferToBuffer(lightDirectionBufferUpdate, 0, lightDirectionBuffer, 0, 3 * Float32Array.BYTES_PER_ELEMENT);
const passEncoderCleanup = commandEncoder.beginRenderPass(renderPassCleanupDesc);
passEncoderCleanup.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoderCleanup.end();
for (let p = 0; p < 6; p++) {
const passEncoder0 = p % 2 == 0 ? commandEncoder.beginRenderPass(renderPassDesc0) : commandEncoder.beginRenderPass(renderPassDesc1);
passEncoder0.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
teapot.encodeRenderPass(passEncoder0, pipeline, p);
passEncoder0.end()
const passEncoder1 = commandEncoder.beginRenderPass(renderPassBlend);
passEncoder1.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
blend.encodeRenderPass(passEncoder1);
passEncoder1.end()
}
const finalEncoder = commandEncoder.beginRenderPass(renderPassFinal);
final.encodeRenderPass(finalEncoder);
finalEncoder.end();
device!.queue.submit([commandEncoder.finish()]);
await device!.queue.onSubmittedWorkDone();
if (projectionMatrixUniformBufferUpdate) {
projectionMatrixUniformBufferUpdate.destroy();
}
modelViewMatrixUniformBufferUpdate.destroy();
normalMatrixUniformBufferUpdate.destroy();
viewDirectionUniformBufferUpdate.destroy();
lightDirectionBufferUpdate.destroy();
angle += 0.01;
requestAnimationFrame(render);
}
new Controls(canvas, arcball, render);
requestAnimationFrame(render);
}
У нас є п'ять типів рендер-проходів: очищення, рендеринг для текстури глибини 0, рендеринг для текстури глибини 1, рендер-прохід для змішування та фінальний рендер-прохід, кожен з яких має відповідні текстури призначення.
Наш перший рендер-прохід очистить нашу текстуру призначення, надаючи їй альфу 1 та повністю чорний RGB колір.
Потім ми пройдемо шість ітерацій зняття глибини.
Спочатку ми рендеримо наш чайник з одним з двох дескрипторів рендер-проходів — ці дескриптори будуть чергуватися протягом наших шести ітерацій — результат цього проходу зберігається в colorTextureForDebugging. Потім ми маємо прохід змішування, який зчитує дані з colorTextureForDebugging і записує їх у нашу текстуру призначення.
Коли ми завершимо наші шість ітерацій, ми використаємо наш фінальний клас змішування, щоб взяти дані, накопичені в текстурі призначення, і вивести їх на екран.
Висновок
У цій статті ми побачили, як можна реалізувати прозорість за допомогою зняття глибини. Ми виконали кілька етапів рендерингу різних шарів на текстуру для накопичення значень, а також зберігали значення глибини попередніх проходів, щоб допомогти вибрати, які фрагменти використовувати в наступних проходах. Зрештою, ми взяли значення текстури призначення і відрендерили їх на екран.
Перекладено з: WebGPU Rendering: Part 15 Transparency with Depth Peeling