3.2 오브젝트 픽킹

3D 그래픽스에서 중요한 또 다른 형태의 상호작용은 오브젝트 픽킹 또는 선택입니다. 예를 들어, RTS 게임에서는 마우스를 사용하여 제어하려는 유닛을 선택합니다.

레이 캐스팅

플레이그라운드 실행 - 3_02_1_picking_ray_casting

픽킹을 구현하는 두 가지 일반적인 방법이 있습니다. 이 튜토리얼에서는 두 가지 방법을 모두 살펴보겠습니다. 첫 번째 방법은 레이 캐스팅(Ray casting)이라고 불리며 그 원리는 간단합니다. 투영 행렬을 적용하여 3D 점을 화면 평면에 투영할 수 있다는 것을 기억하십시오. 레이 캐스팅은 단순히 이 과정을 역전시킵니다. 화면 평면(마우스 커서 위치)의 2D 위치가 주어지면 투영 행렬을 사용하여 광선을 얻습니다. 이 광선은 투영되면 2D 점과 겹쳐집니다. 그런 다음 이 광선을 사용하여 교차할 수 있는 개체에 대해 테스트합니다. 교차가 발생하면 관심 있는 개체를 선택합니다.

레이 캐스팅을 통한 오브젝트 픽킹
레이 캐스팅을 통한 오브젝트 픽킹

객체가 찻주전자처럼 복잡한 모양을 가지고 있다면 정밀한 광선-객체 교차를 계산하는 것은 어려울 수 있습니다. 일반적으로 우리는 계산을 위해 겹치는 프록시 지오메트리를 사용합니다. 예를 들어 경계 상자(bounding box)나 경계 구(bounding sphere)가 있습니다. 일반적으로 광선 교차는 간단한 공식을 사용하여 프록시 지오메트리에 대해 효율적으로 확인할 수 있습니다. 이 튜토리얼에서는 프록시 구를 사용할 것입니다.

이 데모에서는 인스턴싱(instancing) 기법을 사용하여 링 형태의 찻주전자 16개를 렌더링할 것입니다. 각 찻주전자는 프록시 구로 둘러싸여 있습니다. 프록시 구는 시각화 목적으로 렌더링됩니다. 레이 캐스팅은 주로 자바스크립트의 CPU 코드에 의해 계산되므로 렌더링에 대한 대부분의 설명은 생략하겠습니다. 자세한 내용은 샘플 코드를 참조하십시오. 여기서는 장면 설정에 집중하고자 합니다.

프록시 지오메트리로 구를 선택한 주된 이유는 다른 프록시 지오메트리에 비해 구와 광선의 교차 계산이 쉽기 때문입니다. 하지만 구를 사용하는 것이 가장 정확한 옵션은 아닙니다.

첫 번째 부분은 프록시 구를 나타내는 세 개의 링을 구성하는 것입니다. 이는 이전 챕터에서 생성했던 것과 유사합니다. 세 개의 링은 서로 수직인데, 이는 프록시 구의 실루엣을 나타내기 위해 사용되기 때문입니다.

const teapotCenter = glMatrix.vec4.fromValues(center[0], center[1], center[2], 1.0);

console.log("teapot center", teapotCenter);

this.ringPositionBuffer = [];

for (let i = 0; i < 16; ++i) {
    const angle = 2.0 * Math.PI * i / 16.0;
    const x = Math.cos(angle) * radius;
    const y = Math.sin(angle) * radius;

    this.ringPositionBuffer.push(x + center[0]);
    this.ringPositionBuffer.push(y + center[1]);
    this.ringPositionBuffer.push(0.0 + center[2]);
}

this.ringPositionBuffer = createGPUBuffer(device, new Float32Array(this.ringPositionBuffer), GPUBufferUsage.VERTEX);

this.ringPositionBuffer2 = [];
for (let i = 0; i < 16; ++i) {
    const angle = 2.0 * Math.PI * i / 16.0;
    const x = Math.cos(angle) * radius;
    const z = Math.sin(angle) * radius;

    this.ringPositionBuffer2.push(x + center[0]);
    this.ringPositionBuffer2.push(0.0 + center[1]);
    this.ringPositionBuffer2.push(z + center[2]);
}

this.ringPositionBuffer2 = createGPUBuffer(device, new Float32Array(this.ringPositionBuffer2), GPUBufferUsage.VERTEX);


this.ringPositionBuffer3 = [];
for (let i = 0; i < 16; ++i) {
    const angle = 2.0 * Math.PI * i / 16.0;
    const y = Math.cos(angle) * radius;
    const z = Math.sin(angle) * radius;

    this.ringPositionBuffer3.push(0.0 + center[0]);
    this.ringPositionBuffer3.push(y + center[1]);
    this.ringPositionBuffer3.push(z + center[2]);
}
this.ringPositionBuffer3 = createGPUBuffer(device, new Float32Array(this.ringPositionBuffer3), GPUBufferUsage.VERTEX);

여기서 우리는 찻주전자 중심에 위치한 링의 정점들을 포함하는 세 개의 위치 버퍼를 생성합니다. 다음으로, 각 찻주전자 인스턴스에 대한 개별 회전 및 변환을 설정할 것입니다.

for (let i = 0; i < this.instanceCount; ++i) {

    const angle = 2.0 * Math.PI * i / this.instanceCount;

    let translation = glMatrix.mat4.fromTranslation(glMatrix.mat4.create(),
        glMatrix.vec3.fromValues(Math.cos(angle) * circleRadius,
            Math.sin(angle) * circleRadius, 0));
    let rotation = glMatrix.mat4.fromRotation(glMatrix.mat4.create(), Math.PI * 0.5 + angle, glMatrix.vec3.fromValues(0.0, 0.0, 1.0));

    const modelViewMatrix = glMatrix.mat4.multiply(glMatrix.mat4.create(), translation, rotation);

    transformationMats.set(modelViewMatrix, i * 16);

    let modelViewMatrixInverse = glMatrix.mat4.invert(glMatrix.mat4.create(), modelViewMatrix);

    let normalMatrix = glMatrix.mat4.transpose(glMatrix.mat4.create(), modelViewMatrixInverse);

    normalMats.set(normalMatrix, i * 16);

    let center = glMatrix.vec4.transformMat4(glMatrix.vec3.create(), teapotCenter, modelViewMatrix);
    this.instanceCenters.push(glMatrix.vec3.fromValues(center[0], center[1], center[2]));

    console.log('center ', center)
}

각 찻주전자에 대해 먼저 회전을 적용한 다음 모든 인스턴스가 링을 형성하도록 위치를 이동합니다. modelViewMatrix는 배열에 저장되며, 마찬가지로 노멀 행렬도 다른 배열에 보관됩니다. 이 배열들은 다음과 같이 셰이더 프로그램으로 전달됩니다:

@group(0) @binding(3)
var teapotTransformationMat : array, 16>;
@group(0) @binding(4)
var teapotNormalMat : array, 16>;
• • •
@vertex
fn vs_main(
    @builtin(instance_index) instanceIdx : u32,
    @location(0) inPos: vec3,
    @location(1) inNormal: vec3
    //,@location(2) transformation: mat4x4
) -> 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 * teapotNormalMat[instanceIdx] * vec4(inNormal, 0.0)).xyz;  
    
    out.clip_position = projection * modelView * teapotTransformationMat[instanceIdx] * vec4(inPos, 1.0);

    if (selected == instanceIdx) {
        out.normal = vec3(0.0, 0.0, 0.0);
    }
    return out;
}

정점 셰이더의 주 함수는 @builtin(instance_index) instanceIdx : u32 입력을 포함합니다. 이 내장 변수는 이전에 보았던 vertex_index와 유사하게 인스턴스 ID를 제공합니다. 이 인덱스를 사용하여 각 인스턴스에 해당하는 변환 및 노멀 행렬을 가져옵니다.

변환된 찻주전자 중심도 배열에 저장합니다. 이들은 레이-구 교차 테스트에 중요합니다.

이제 마우스 이벤트 핸들링 함수에 구현된 픽킹(picking) 기능을 살펴보겠습니다. 이 데모는 아크볼(arcball)을 통한 내비게이션도 지원하므로, onmousedown 함수와 같은 대부분의 구현은 이전 챕터에서 다룬 내용과 유사합니다. 여기서는 특히 mousemove 이벤트 핸들러의 픽킹 로직에 중점을 두겠습니다:

//selection mode
const currX = (x - originX) * 2.0 / width;
const currY = (originY - y) * 2.0 / height;
//https://gamedev.stackexchange.com/questions/17987/what-does-the-graphics-card-do-with-the-fourth-element-of-a-vector-as-the-final
//https://antongerdelan.net/opengl/raycasting.html
//https://gamedev.stackexchange.com/questions/153078/what-can-i-do-with-the-4th-component-of-gl-position
let projectionMatrixInverse = glMatrix.mat4.invert(glMatrix.mat4.create(), projectionMatrix);

let clipSpacePosition = glMatrix.vec4.fromValues(currX, currY, 0.0, 1.0);

let camSpacePosition = glMatrix.vec4.transformMat4(glMatrix.vec4.create(), clipSpacePosition, projectionMatrixInverse);

//clipSpacePosition[2] = -1;
camSpacePosition[3] = 0;

let dir = glMatrix.vec4.transformMat4(glMatrix.vec4.create(), camSpacePosition, modelViewMatrixInverse);

dir = glMatrix.vec3.normalize(glMatrix.vec3.create(), dir);
let found = false;
console.log("select", currX, currY, teapot.instanceCount);
for (let i = 0; i < teapot.instanceCount; ++i) {
    let center = teapot.instanceCenters[i];
    center = glMatrix.vec3.fromValues(center[0] - arcball.forwardVector[0],
        center[1] - arcball.forwardVector[1],
        center[2] - arcball.forwardVector[2]);

    const dot = glMatrix.vec3.dot(center, dir);
    const dis = glMatrix.vec3.length(glMatrix.vec3.subtract(glMatrix.vec3.create(), glMatrix.vec3.scale(glMatrix.vec3.create(), dir, dot), center));
    console.log("dis ", dis);
    if (dis < teapot.teapotRadius) {
        console.log('found teapot ', i, dis, teapot.teapotRadius);
        const selectionUniformBufferUpdate = createGPUBuffer(device, new Uint32Array([i]), GPUBufferUsage.COPY_SRC);
        commandEncoder = device.createCommandEncoder();
        commandEncoder.copyBufferToBuffer(selectionUniformBufferUpdate, 0,
            teapot.selectionUniformBuffer, 0, 4);
        device.queue.submit([commandEncoder.finish()]);
        requestAnimationFrame(render);
        console.log("update selection")
        found = true;
        break;
    }
}
if (!found) {
    const selectionUniformBufferUpdate = createGPUBuffer(device, new Uint32Array([17]), GPUBufferUsage.COPY_SRC);
    commandEncoder = device.createCommandEncoder();
    commandEncoder.copyBufferToBuffer(selectionUniformBufferUpdate, 0,
        teapot.selectionUniformBuffer, 0, 4);
    device.queue.submit([commandEncoder.finish()]);
    requestAnimationFrame(render);
}

이 함수에는 마우스가 드래그되고 있는지 확인하는 조건이 포함되어 있습니다. 드래그 중인 경우, 입력을 아크볼 내비게이션 제스처로 간주하고 아크볼을 그에 따라 업데이트합니다. 아무 버튼도 누르지 않고 마우스가 움직이는 경우, 이를 픽킹 시나리오로 간주합니다.

const currX = (x - originX) * 2.0 / width;
const currY = (originY - y) * 2.0 / height;
//https://gamedev.stackexchange.com/questions/17987/what-does-the-graphics-card-do-with-the-fourth-element-of-a-vector-as-the-final
//https://antongerdelan.net/opengl/raycasting.html
//https://gamedev.stackexchange.com/questions/153078/what-can-i-do-with-the-4th-component-of-gl-position
let projectionMatrixInverse = glMatrix.mat4.invert(glMatrix.mat4.create(), projectionMatrix);

let clipSpacePosition = glMatrix.vec4.fromValues(currX, currY, 0.0, 1.0);

let camSpacePosition = glMatrix.vec4.transformMat4(glMatrix.vec4.create(), clipSpacePosition, projectionMatrixInverse);

//clipSpacePosition[2] = -1;
camSpacePosition[3] = 0;

let dir = glMatrix.vec4.transformMat4(glMatrix.vec4.create(), camSpacePosition, modelViewMatrixInverse);

dir = glMatrix.vec3.normalize(glMatrix.vec3.create(), dir);

위 코드의 목적은 캔버스 상의 현재 마우스 위치를 월드 공간(world space)에서의 광선 방향으로 변환하는 것입니다. 이를 이해하려면 그래픽스 파이프라인의 좌표 변환에 익숙해야 합니다. 투영 행렬을 적용한 후 3D 점은 카메라 좌표계에서 클립 공간(clip space)으로 변환됩니다. 클립 공간 위치는 x, y, z 좌표에 대해 -1.0에서 1.0까지의 범위를 가집니다. 따라서 계산은 x와 y가 이 정규화된 범위 내에 있도록 보장합니다.

const currX = (x - originX) * 2.0 / width;
const currY = (originY - y) * 2.0 / height;

z축의 경우, 화면 위치가 2D이므로 clipSpacePosition에 z 값을 할당해야 합니다. 여기서는 0을 사용합니다. w 성분은 동차 좌표(homogeneous coordinates)에서 마지막 성분이 1.0이면 위치를 나타내고, 그렇지 않으면 방향을 나타낸다는 것을 상기하십시오. 지금은 이를 위치로 취급하겠습니다. 그런 다음 역 투영 행렬을 적용하여 클립 공간 점을 카메라 좌표계로 변환합니다. 카메라 좌표계에서 원점은 카메라에 있습니다. 따라서 카메라에서 마우스 위치를 통해 장면으로 쏘는 광선은 점의 위치에서 원점을 뺀 것으로 표현됩니다. 카메라 좌표의 w 성분을 0으로 설정하는 이유는 그것이 위치가 아닌 방향을 나타내기 때문입니다. 마지막으로 이 방향에 역 모델-뷰 행렬을 적용하여 월드 공간에서의 방향을 얻고 정규화합니다.

for (let i = 0; i < teapot.instanceCount; ++i) {
    let center = teapot.instanceCenters[i];
    center = glMatrix.vec3.fromValues(center[0] - arcball.forwardVector[0],
        center[1] - arcball.forwardVector[1],
        center[2] - arcball.forwardVector[2]);

    const dot = glMatrix.vec3.dot(center, dir);
    const dis = glMatrix.vec3.length(glMatrix.vec3.subtract(glMatrix.vec3.create(), glMatrix.vec3.scale(glMatrix.vec3.create(), dir, dot), center));
    console.log("dis ", dis);
    if (dis < teapot.teapotRadius) {
        console.log('found teapot ', i, dis, teapot.teapotRadius);
        const selectionUniformBufferUpdate = createGPUBuffer(device, new Uint32Array([i]), GPUBufferUsage.COPY_SRC);
        commandEncoder = device.createCommandEncoder();
        commandEncoder.copyBufferToBuffer(selectionUniformBufferUpdate, 0,
            teapot.selectionUniformBuffer, 0, 4);
        device.queue.submit([commandEncoder.finish()]);
        requestAnimationFrame(render);
        console.log("update selection")
        found = true;
        break;
    }
}

다음으로, 모든 찻주전자를 순회합니다. 각 찻주전자에 대해 중심에서 카메라 위치까지의 방향을 계산합니다. 이 벡터를 광선 벡터에 투영한 다음, 투영된 길이에 따라 광선 벡터의 스케일을 조정합니다. 마지막으로, 스케일링된 광선 벡터에서 찻주전자의 중심 벡터를 뺍니다. 그 결과 벡터는 광선 벡터에 수직이며, 찻주전자 중심에서 광선까지의 최단 거리를 나타냅니다. 이 거리가 찻주전자 프록시 구의 반지름보다 작으면, 광선이 찻주전자와 교차한다고 간주합니다.

이 경우, 유니폼 버퍼에서 현재 선택된 찻주전자 인덱스를 업데이트합니다. 셰이더 코드에서는 인스턴스 ID가 현재 선택과 일치하면 찻주전자가 다른 색상으로 렌더링됩니다.

선택이 감지되지 않으면, 현재 선택 ID를 17로 설정하여 이전 선택을 재설정합니다. 찻주전자가 16개뿐이므로 셰이더에서 ID 17과 일치하는 것이 없어 모든 찻주전자가 정상적으로 렌더링됩니다.

if (!found) {
    const selectionUniformBufferUpdate = createGPUBuffer(device, new Uint32Array([17]), GPUBufferUsage.COPY_SRC);
    commandEncoder = device.createCommandEncoder();
    commandEncoder.copyBufferToBuffer(selectionUniformBufferUpdate, 0,
        teapot.selectionUniformBuffer, 0, 4);
    device.queue.submit([commandEncoder.finish()]);
    requestAnimationFrame(render);
}

마지막으로, 인스턴스 렌더링을 위한 드로우 커맨드를 인코딩하는 방법을 살펴보겠습니다. 인스턴스 렌더링의 장점은 동일한 지오메트리를 효율적으로 복제할 수 있다는 점으로, 게임에서 나뭇잎이나 파티클을 렌더링하는 데 이상적입니다. 이 예제에서 인스턴스화된 객체를 렌더링하는 것은 간단합니다. 드로우 커맨드를 인코딩할 때 렌더링할 인스턴스의 총 개수를 지정하기만 하면 됩니다. 이전에 논의했듯이 각 인스턴스는 instanceIdx를 기반으로 유니폼 버퍼에서 가져오는 자체 변환 행렬을 가집니다.

encode(encoder) {
    encoder.setPipeline(this.pipeline);
    encoder.setBindGroup(0, this.uniformBindGroup);
    encoder.setVertexBuffer(0, this.positionBuffer);
    encoder.setVertexBuffer(1, this.normalBuffer);
    encoder.setIndexBuffer(this.indexBuffer, 'uint16');
    encoder.drawIndexed(this.indexSize, this.instanceCount);
    encoder.setPipeline(this.ringPipeline);
    encoder.setBindGroup(0, this.ringUniformBindGroup);
    encoder.setVertexBuffer(0, this.ringPositionBuffer);
    encoder.draw(16, this.instanceCount);
    encoder.setVertexBuffer(0, this.ringPositionBuffer2);
    encoder.draw(16, this.instanceCount);
    encoder.setVertexBuffer(0, this.ringPositionBuffer3);
    encoder.draw(16, this.instanceCount);
}

레이 캐스팅 오브젝트 픽킹. 선택된 객체는 검은색으로 표시됩니다.
레이 캐스팅 오브젝트 픽킹. 선택된 객체는 검은색으로 표시됩니다.

컬러 코딩

플레이그라운드 실행 - 3_02_2_picking_color_coding

레이 캐스팅은 구현이 비교적 간단하지만, 일부 시나리오에서는 한계가 있습니다. 예를 들어, 프록시 지오메트리는 실제 객체의 세부 사항이 부족하여 픽킹 정밀도에 영향을 미칠 수 있습니다. 또한 우리가 구현한 레이 캐스팅 방법은 오클루전(occlusion)을 고려하지 않습니다. 즉, 교차 감지 중에 멀리 떨어져 가려진 객체가 먼저 발견되면 여전히 선택될 수 있습니다. 더욱이, 단면만 가진 객체(예: 앞면만 있는 평면)의 경우, 후면 컬링을 하면 뷰어로부터 멀어질 때 객체가 보이지 않아야 합니다. 그러나 레이 픽킹은 여전히 해당 객체를 선택할 것입니다. 마지막으로, 객체가 많을 경우 특히 많은 객체가 가려져 있거나 시야 밖에 있을 때 레이 캐스팅은 느려질 수 있습니다. 이 문제는 k-d 트리와 같이 광선 교차 테스트를 광선과 교차할 가능성이 있는 객체로 제한하는 더 효율적인 장면 데이터 구조를 사용하여 완화할 수 있습니다.

색상으로 코딩된 3D 장면의 예시
색상으로 코딩된 3D 장면의 예시

이 섹션에서는 이러한 문제 중 일부를 해결하는 컬러 코딩(Color coding)이라는 대체 픽킹 방법을 살펴보겠습니다. 컬러 코딩의 원리는 간단합니다. 각 객체의 ID를 고유한 색상으로 인코딩합니다. RGB 색상으로는 최대 2^24개의 객체를 나타낼 수 있으며, 이는 대부분의 사용 사례에 충분합니다. 이러한 고유 색상을 사용하여 장면을 렌더링한 다음 렌더링된 이미지를 다시 읽어옵니다. 이 단계에서는 블렌딩이나 앤티앨리어싱과 같이 색상 값을 변경할 수 있는 프로세스를 피하는 것이 중요합니다. 렌더링 후에는 마우스 커서 아래의 색상을 확인하여 색상에서 객체 ID를 검색할 수 있습니다. 전체 이미지를 읽는 대신, 커서 주변의 작은 영역을 샘플링하는 것으로 충분합니다. 또한 픽킹은 일반적으로 픽셀 단위의 정확도를 요구하지 않으므로 해상도를 낮춰 렌더링하면 성능을 향상시킬 수 있습니다.

컬러 코딩의 이점은 명확합니다. 장면을 고유한 색상으로 렌더링함으로써 가장 앞에 있고 보이는 객체만 선택 대상으로 고려됩니다. 이 방법은 또한 프록시 지오메트리가 필요 없어 실제 지오메트리로 정확한 픽킹이 가능합니다. 그러나 레이 캐스팅과 달리 컬러 코딩은 셰이더 코드와 자바스크립트 코드 간의 조율이 필요합니다.

이제 코드를 살펴보겠습니다:

@vertex
fn vs_main(
    @builtin(instance_index) instanceIdx : u32,
    @location(0) inPos: vec3,
    @location(1) color: vec4
) -> VertexOutput {
    var out: VertexOutput;
    out.clip_position = projection * modelView * teapotTransformationMat[instanceIdx] * vec4(inPos, 1.0);
    out.color = color.xyz;
    return out;
}

// Fragment shader

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4 {
    return vec4( in.color.xyz ,255);
}

이 과정에는 두 가지 셰이더가 사용됩니다. 첫 번째 셰이더는 일반 렌더링 셰이더인데, 이는 생략하겠습니다. 두 번째 셰이더는 컬러 코딩 렌더링에 사용되며, 실제로는 더 간단합니다. 컬러 코드는 정점 속성(vertex attributes)을 통해 전달된 후 프래그먼트 셰이더로 전달되어 이 색상들을 렌더링합니다. 투명도 채널은 완전히 불투명하게 설정되며, 출력 형식은 색상 값을 부동 소수점으로 스케일링하는 것을 방지하기 위해 u32로 설정하여 색상 코드가 변경될 가능성을 없앱니다.

bufferWidth = (Math.floor(2 * 4 / 256.0) + 1) * 256;
copiedBuffer = createGPUBuffer(device, new Uint8Array(bufferWidth * 2), GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ);
• • •
async function pick(x, y) {
    if (inRendering) {
        return 17;
    }
    inRendering = true;
• • •
passEncoder = commandEncoder.beginRenderPass(renderPassDescColorCode);

passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
teapot.encodeForColorCoding(passEncoder);

passEncoder.end();
commandEncoder.copyTextureToBuffer({ texture: colorCodeTexture, origin: { x, y } }, { buffer: copiedBuffer, bytesPerRow: bufferWidth }, { width: 2, height: 2 });
device.queue.submit([commandEncoder.finish()]);

await device.queue.onSubmittedWorkDone();
• • •
        await copiedBuffer.mapAsync(GPUMapMode.READ, 0, bufferWidth * 2);
        //const imageData = new ImageData(new Uint8ClampedArray(copiedBuffer.getMappedRange()), bufferWidth / 4, 2);

        const d = new Uint8ClampedArray(copiedBuffer.getMappedRange());
        const picked = d[0];
        console.log('pick ', picked);

        copiedBuffer.unmap();
        inRendering = false;

        return picked;
    }

    return 17;
}

먼저, 이전 호출이 아직 처리 중일 때 픽(pick) 함수가 호출되는 것을 방지하기 위해 inRendering 플래그를 사용합니다. 이렇게 하면 임시 버퍼가 매핑된 상태에서 기록되지 않도록 하여 잠재적인 문제를 방지할 수 있습니다.

렌더링 후, 프레임버퍼를 임시 copiedBuffer로 복사합니다. 이 버퍼는 프레임버퍼보다 작으며, 현재 마우스 커서 주변의 2x2 픽셀 창만 복사합니다. 사양에 따르면 bytesPerRow 매개변수는 256의 배수여야 하므로, 이 요구 사항을 준수하기 위해 필요한 것보다 더 많은 공간을 할당합니다.

명령 큐가 완료되면, 복사된 버퍼를 읽기 접근을 위해 매핑합니다. 더 정교한 구현은 가장 지배적인 색상을 분석하여 가장 가능성 있는 객체를 결정할 수 있지만, 이 예제에서는 첫 번째 픽셀의 빨간색 채널만 확인합니다. 데모에 16개의 객체만 있으므로 빨간색 채널만으로 객체 인덱스를 식별하기에 충분하므로 이 접근 방식을 단순하게 사용합니다.

이제 파이프라인 설정 코드를 살펴보겠습니다. 특히, 찻주전자 인스턴스를 설정할 때 객체 인덱스를 색상으로 인코딩하는 방법입니다. 객체 인덱스를 리틀 엔디언(little-endian) 형식으로 RGB 값에 인코딩합니다.

for (let i = 0; i < this.instanceCount; ++i) {
• • •
    colorCodes.set([i & 0b11111111,
    (i >> 8) & 0b11111111, (i >> 16) & 0b11111111, (i >> 24) & 0b11111111], i * 4 + 0);
}

마지막 단계는 컬러 코딩된 장면을 렌더링하기 위한 대상 텍스처를 생성하는 것입니다.

const colorCodeTextureDesc = {
    size: [canvas.width, canvas.height, 1],
    dimension: '2d',
    format: 'rgba8uint',
    usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
};

colorCodeTexture = device.createTexture(colorCodeTextureDesc);

let colorCodeTextureView = colorCodeTexture.createView();

colorCodeAttachment = {
    view: colorCodeTextureView,
    clearValue: { r: 1, g: 0, b: 0, a: 1 },
    loadOp: 'clear',
    storeOp: 'store'
};

텍스처의 rgba8unorm 형식은 셰이더의 출력 형식과 일치하도록 사용됩니다. 읽기 버퍼의 경우, bufferWidth를 어떻게 결정하는지 주목하십시오. 2x2 창의 단일 행은 4 * 2 바이트만 필요하지만, 256의 가장 가까운 배수로 올림 되어야 합니다. 이는 GPUImageCopyBuffer의 요구 사항입니다.

이것으로 픽킹 설정이 완료됩니다. 생략하기로 선택한 일반 렌더링 부분은 이 과정을 보완합니다.

컬러 코딩 기반 오브젝트 픽킹은 프록시 지오메트리가 필요 없습니다.
컬러 코딩 기반 오브젝트 픽킹은 프록시 지오메트리가 필요 없습니다.

GitHub에 의견 남기기