光线追踪(Ray Tracing) 阴影映射

145 阅读5分钟

简介

如果在光源处放置一位观察者,其视线方向与光线一致,那么观察者也看不到阴影。他看到的每一处都在光的照射下,而那些背后的,他没有看到的物体则处在阴影中。这里,我们需要用到光源与物体之间的距离(实际上也就是物体在光源坐标系下的深度z值)来决定物体是否可见。

可以使用两对着色器以实现阴影

  • 一对着色器用来计算光源到物体的距离。
  • 另一对着色器根据[1]中计算出的距离绘制场景。
  • 使用一张纹理图像把[1]的结果传入[2]中,这张纹理图像就被称为阴影贴图(shadow map),而通过阴影贴图实现阴影的方法就被称为阴影映射(shadow mapping)。

如何引入阴影

通过分析,一个点不在阴影里的条件是,光源、相机能同时看到点,光源看不到、相机能看到的,就是阴影区域。 第一步:从光源位置出发找出可见点,记录光源可见点的深度,得到光源深度图。 第二步:从相机出发,找可见点,如果点可见,坐标变换求此点到光源的距离。如果此距离与光源深度图中此位置的深度一致,说明此点可以被光源照到。是为光源、相机能同时看到点;如果不一致,说明是阴影点。

改造initPipeline 增加阴影管线

// 抽离公用配置 ///////////////////////////////////////////////////////
//顶点缓冲区配置
const vertexBuffers = [{
    arrayStride: 8 * 4,
    attributes: [
        {
            shaderLocation: 0,
            offset: 0,
            format: 'float32x3',
        },
        {
            shaderLocation: 1,
            offset: 3 * 4,
            format: 'float32x3',
        },
        {
            shaderLocation: 2,
            offset: 6 * 4,
            format: 'float32x2',
        },
    ]
}]
//图元的状态
const primitive = {
    topology: 'triangle-list',
    cullMode: 'back'
}
//深度模板
const depthStencil = {
    depthWriteEnabled: true,
    depthCompare: 'less',
    format: 'depth32float',
}

//一、投影管线///////////////////////////////////////////
const shadowPipeline = await device.createRenderPipelineAsync({
    label: 'Shadow Pipline',
    layout: 'auto',
    vertex: {
        module: device.createShaderModule({
            code: shadowDepth,
        }),
        entryPoint: 'main',
        buffers: vertexBuffers
    },
    primitive, depthStencil
})
//投影缓存
const shadowDepthTexture = device.createTexture({
    size: [2048, 2048],
    usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
    format: 'depth32float'
});
const shadowDepthView = shadowDepthTexture.createView()


//二、渲染管线///////////////////////////////////////////
const renderPipeline = await device.createRenderPipelineAsync({
    label: 'Render Pipline',
    layout: 'auto',
    vertex: {
        module: device.createShaderModule({
            code: shadowVertex,
        }),
        entryPoint: 'main',
        buffers: vertexBuffers
    },
    fragment: {
        module: device.createShaderModule({
            code: shadowFrag,
        }),
        entryPoint: 'main',
        targets: [
            {
                format: format
            }
        ]
    },
    primitive, depthStencil
})
//深度缓存
const renderDepthTexture = device.createTexture({
    size, format: 'depth32float',
    usage: GPUTextureUsage.RENDER_ATTACHMENT
})
const renderDepthView = renderDepthTexture.createView()


//顶点数据缓冲区//////////////////////////////////////
// 加入了立方体,球形两种模型 参考index渲染


//mvp矩阵、灯光、阴影 ////////////////////////////////////////////////////////
const modelViewBuffer = device.createBuffer({
    label: 'GPUBuffer store n*4x4 matrix',
    size: 4 * 4 * 4 * NUM, // 4 x 4 x float32 x NUM
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
})
const cameraProjectionBuffer = device.createBuffer({
    label: 'GPUBuffer for camera projection',
    size: 4 * 4 * 4, // 4 x 4 x float32
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
})
//灯光投影
const lightProjectionBuffer = device.createBuffer({
    label: 'GPUBuffer for light projection',
    size: 4 * 4 * 4, // 4 x 4 x float32
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
})
// 颜色
const colorBuffer = device.createBuffer({
    label: 'GPUBuffer store n*4 color',
    size: 4 * 4 * NUM, // 4 x float32 x NUM
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
})
//灯光
const lightBuffer = device.createBuffer({
    label: 'GPUBuffer store 4x4 matrix',
    size: 4 * 4, // 4 x float32: position vec4
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
})

//绑定组////////////////////////////////////////////////////
//模型矩阵、视图矩阵、灯光投影、颜色
const vsGroup = device.createBindGroup({
    label: 'Group for renderPass',
    layout: renderPipeline.getBindGroupLayout(0),
    entries: [
        {
            binding: 0,
            resource: {
                buffer: modelViewBuffer
            }
        },
        {
            binding: 1,
            resource: {
                buffer: cameraProjectionBuffer
            }
        },
        {
            binding: 2,
            resource: {
                buffer: lightProjectionBuffer
            }
        },
        {
            binding: 3,
            resource: {
                buffer: colorBuffer
            }
        }
    ]
})
//灯光、阴影
const fsGroup = device.createBindGroup({
    label: 'Group for fragment',
    layout: renderPipeline.getBindGroupLayout(1),
    entries: [
        {
            binding: 0,
            resource: {
                buffer: lightBuffer
            }
        },
        {
            binding: 1,
            resource: shadowDepthView
        },
        {
            binding: 2,
            resource: device.createSampler({
                compare: 'less',
            })
        }
    ]
})
//投影管线组
const shadowGroup = device.createBindGroup({
    label: 'Group for shadowPass',
    layout: shadowPipeline.getBindGroupLayout(0),
    entries: [{
        binding: 0,
        resource: {
            buffer: modelViewBuffer
        }
    }, {
        binding: 1,
        resource: {
            buffer: lightProjectionBuffer
        }
    }]
})

改造 draw 绘制阴影

//绘制阴影管线,输出阴影贴图 pipelineObj.shadowDepthView
{
    const shadowPassDescriptor = {
        colorAttachments: [],
        depthStencilAttachment: {
            view: pipelineObj.shadowDepthView,
            depthClearValue: 1.0,
            depthLoadOp: 'clear',
            depthStoreOp: 'store',
        }
    }
    // 阴影通道的编码过程
    const shadowPass = commandEncoder.beginRenderPass(shadowPassDescriptor)
    // 使用阴影渲染管线
    shadowPass.setPipeline(pipelineObj.shadowPipeline)
    shadowPass.setBindGroup(0, pipelineObj.shadowGroup)
    // set box vertex
    shadowPass.setVertexBuffer(0, pipelineObj.boxBuffer.vertex)
    shadowPass.setIndexBuffer(pipelineObj.boxBuffer.index, 'uint16')
    shadowPass.drawIndexed(box.indexCount, 2, 0, 0, 0)
    // set sphere vertex
    shadowPass.setVertexBuffer(0, pipelineObj.sphereBuffer.vertex)
    shadowPass.setIndexBuffer(pipelineObj.sphereBuffer.index, 'uint16')
    shadowPass.drawIndexed(sphere.indexCount, NUM - 2, 0, 0, 2)
    shadowPass.end()
}

// 绘制渲染管线
{
    const renderPassDescriptor = {
        colorAttachments: [
            {
                view: context.getCurrentTexture().createView(),
                clearValue: { r: 0, g: 0, b: 0, a: 1.0 },
                loadOp: 'clear',
                storeOp: 'store'
            }
        ],
        depthStencilAttachment: {
            view: pipelineObj.renderDepthView,
            depthClearValue: 1.0,
            depthLoadOp: 'clear',
            depthStoreOp: 'store',
        }
    }
    // 渲染通道编码过程
    const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor)
    // 使用常规渲染管线
    renderPass.setPipeline(pipelineObj.renderPipeline)
    renderPass.setBindGroup(0, pipelineObj.vsGroup)
    renderPass.setBindGroup(1, pipelineObj.fsGroup)
    // set box vertex
    renderPass.setVertexBuffer(0, pipelineObj.boxBuffer.vertex)
    renderPass.setIndexBuffer(pipelineObj.boxBuffer.index, 'uint16')
    renderPass.drawIndexed(box.indexCount, 2, 0, 0, 0)
    // set sphere vertex
    renderPass.setVertexBuffer(0, pipelineObj.sphereBuffer.vertex)
    renderPass.setIndexBuffer(pipelineObj.sphereBuffer.index, 'uint16')
    renderPass.drawIndexed(sphere.indexCount, NUM - 2, 0, 0, 2)
    renderPass.end()
}

改造运行时函数

const NUM = 3;//设置3个模型,地板用于接收阴影,一个正方体、一个球形。

const scene = []
const modelViewMatrix = new Float32Array(NUM * 4 * 4)
const colorBuffer = new Float32Array(NUM * 4)
// 添加正方体
{
    const position = { x: 0, y: 5, z: -20 }
    const rotation = { x: 0, y: Math.PI / 4, z: 0 }
    const scale = { x: 5, y: 5, z: 5 }
    const modelView = getModelViewMatrix(position, rotation, scale)
    modelViewMatrix.set(modelView, 0 * 4 * 4)
    // 颜色
    colorBuffer.set([0.5, 0.5, 0.5, 1], 0 * 4)
    scene.push({ position, rotation, scale })
}
// 添加地板
{
    const position = { x: 0, y: -10, z: -20 }
    const rotation = { x: 0, y: 0, z: 0 }
    const scale = { x: 50, y: 0.5, z: 40 }
    const modelView = getModelViewMatrix(position, rotation, scale)
    modelViewMatrix.set(modelView, 1 * 4 * 4)
    // 颜色
    colorBuffer.set([1, 1, 1, 1], 1 * 4)
    scene.push({ position, rotation, scale })
}
// 添加球体
{
    const position = { x: 0, y: 5, z: -5 }
    const rotation = { x: 0, y: 0, z: 0 }
    const scale = { x: 0.5, y: 0.5, z: 0.5 }
    const modelView = getModelViewMatrix(position, rotation, scale)
    modelViewMatrix.set(modelView, 2 * 4 * 4)
    // 颜色
    colorBuffer.set([1, 0, 1, 1], 2 * 4)
    scene.push({ position, rotation, scale, y: position.y, v: 0 })
}
// 设置模型矩阵、与颜色
device.queue.writeBuffer(pipelineObj.colorBuffer, 0, colorBuffer)
device.queue.writeBuffer(pipelineObj.modelViewBuffer, 0, modelViewMatrix)

// 动态设置平行光坐标,
const lightPosition = vec3.fromValues(0, 100, 0)//灯光坐标
const lightViewMatrix = mat4.create()
const lightProjectionMatrix = mat4.create()
const up = vec3.fromValues(0, 1, 0)
const origin = vec3.fromValues(0, 0, 0)
function frame() {
    // 更新灯光坐标
    const now = performance.now()
    lightPosition[0] = Math.sin(now / 1500) * 50
    lightPosition[2] = Math.cos(now / 1500) * 50
    // 更新 lvp 矩阵
    mat4.lookAt(
        lightViewMatrix,
        lightPosition,
        origin, up
    )
    mat4.ortho(lightProjectionMatrix, -40, 40, -40, 40, -50, 200)
    mat4.multiply(lightProjectionMatrix, lightProjectionMatrix, lightViewMatrix)

    device.queue.writeBuffer(pipelineObj.lightProjectionBuffer, 0, lightProjectionMatrix)//设置灯光投影矩阵
    device.queue.writeBuffer(pipelineObj.lightBuffer, 0, lightPosition)//设置灯光坐标
    ...



shader 修改

阴影管线

阴影管线只输出,阴影深度贴图,不渲染。只有顶点着色器

export const shadowDepth = `@group(0) @binding(0) var<storage> modelViews : array<mat4x4<f32>>;
@group(0) @binding(1) var<uniform> lightProjection : mat4x4<f32>;

@vertex
fn main(
    @builtin(instance_index) index : u32,
    @location(0) position : vec3<f32>,
    @location(1) normal : vec3<f32>,
    @location(2) uv : vec2<f32>,
) -> @builtin(position) vec4<f32> {
    let modelview = modelViews[index];
    let pos = vec4(position, 1.0);
    return lightProjection * modelview * pos;
}
` 

渲染管线

// 顶点着色器 /////////////////////////////////////
// 增加了顶点的,模型坐标与法向量
output.fragPosition = (modelview * pos).xyz;
output.fragNormal =  (modelview * vec4<f32>(normal, 0.0)).xyz;
// 最重要阴影坐标
output.shadowPos = vec3<f32>(posFromLight.xy * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5, 0.5), posFromLight.z);

// 片源着色器 /////////////////////////////////////
let diffuse: f32 = max(dot(normalize(lightPosition.xyz), fragNormal), 0.0);
var shadow : f32 = 0.0;
for (var y : i32 = -1 ; y <= 1 ; y = y + 1) {
    for (var x : i32 = -1 ; x <= 1 ; x = x + 1) {
        let offset = vec2<f32>(f32(x) / size, f32(y) / size);
        shadow = shadow + textureSampleCompare(
            shadowMap, 
            shadowSampler,
            shadowPos.xy + offset, 
            shadowPos.z - 0.005 
        );
    }
}
shadow = shadow / 9.0;//球平均
//输出片源颜色
let lightFactor = min(0.3 + shadow * diffuse, 1.0);
return vec4<f32>(objectColor * lightFactor, 1.0);