개발 · 컴퓨터공학/Physical Simulation

three.js GPGPU SPH - particle integration 구현하기

2024. 10. 24. 11:09
728x90
반응형

 

shader 나누기 

그럼 결국 GLSL 1 버전으로 진행한다.

 

struct Particle{
    float pressure;
    float density;
    vec3 currentForce;
    vec3 velocity;
    vec3 position;
};

이 particle structure를 쪼개서 gl_FragColor에 담아야한다. 

현재 integrate 로직에서는 pressure와 density를 다루지 않으므로 

force, velocity, position 세 개의 스크립트로 나누도록 해보자. 

 

Variable의 순환 

이해하는데 좀 걸렸다. 

일단 아래 코드를 보자.

 

this.initialPosition = this.gpuCompute.createTexture();
this.initialVelocity = this.gpuCompute.createTexture();
this.initialForce = this.gpuCompute.createTexture();

this.positionVariable = this.gpuCompute.addVariable('positionTexture', computeIntegratePosition, this.initialPosition);
this.velocityVariable = this.gpuCompute.addVariable('velocityTexture', computeIntegrateVelocity, this.initialVelocity);
this.forceVariable = this.gpuCompute.addVariable('forceTexture', computeIntegrateForce, this.initialForce);

this.gpuCompute.setVariableDependencies(this.positionVariable, [this.positionVariable, this.velocityVariable]);
this.gpuCompute.setVariableDependencies(this.velocityVariable, [this.velocityVariable, this.forceVariable]);
this.gpuCompute.setVariableDependencies(this.forceVariable, [this.forceVariable]);

이렇게 세 개로 shader를 나눈 상태이다.

여기서 variable을 texture와 동일하게 생각해도 무방한데,

 

결국 setVariable으로 세팅된 variable에는

compute shader의 gl_FragColor로 return되는 값이 반영되는 것이라고 한다.

 

  material.uniforms.uTexture.value = gpuCompute.getCurrentRenderTarget(positionVariable).texture;

이전 테스트 코드에서 compute 이후 variable에서 texture를 가져오는 코드이다.

이걸 보면 결국 compute shader의 결과를 variable에서 get 해오는 것을 볼 수 있다.

 

그렇다는 것은 compute shader를 dispatch하고 얻는 결과는 variable에 저장된다는 것인데, 

이걸 어떻게 업데이트할지 고민하고 있었다.

 

우선 정말 그렇게 잘 동작하는지 테스트를 해보자. 

 

particle position 변경 테스트

//#region position uniform
this.positionVariable.material.uniforms.particleLength = { value: this.totalParticles };
this.positionVariable.material.uniforms.particleMass = { value: 1.0 };
// this.positionVariable.material.uniforms.viscosity = { value: 0.01 };
// this.positionVariable.material.uniforms.gasConstant = { value: 1.4 };
// this.positionVariable.material.uniforms.restDensity = { value: 1000.0 };
this.positionVariable.material.uniforms.boundDamping = { value: 0.9 };
// this.positionVariable.material.uniforms.pi = { value: Math.PI };
this.positionVariable.material.uniforms.boxSize = { value: new THREE.Vector3(10, 10, 10) };

this.positionVariable.material.uniforms.radius = { value: this.particleRadius };
// this.positionVariable.material.uniforms.radius2 = { value: Math.pow(this.particleRadius, 2) };
// this.positionVariable.material.uniforms.radius3 = { value: Math.pow(this.particleRadius, 3) };
// this.positionVariable.material.uniforms.radius4 = { value: Math.pow(this.particleRadius, 4) };
// this.positionVariable.material.uniforms.radius5 = { value: Math.pow(this.particleRadius, 5) };

this.positionVariable.material.uniforms.timestep = { value:  0.016 };
//#endregion

//#region velocity uniform
this.velocityVariable.material.uniforms.timestep = { value:  0.016 };
this.velocityVariable.material.uniforms.particleMass = { value: 1.0 };
this.velocityVariable.material.uniforms.particleLength = { value: this.totalParticles };
//#endregion

//#region force uniform
this.forceVariable.material.uniforms.particleMass = { value: 1.0 };
this.forceVariable.material.uniforms.particleLength = { value: this.totalParticles };
//#endregion

addVariable로 추가한 변수는 shader 끼리 공유 되지만

그 외의 uniform으로 선언한 것들은 glsl shader마다 상수를 추가해주어야한다.

 

그랬더니. 

결과로 뽑은 texture buffet를 읽어서 출력하면 force, velocity, position값이 모두 나오긴 한다. 

 

이걸 particle position에 반영해보자.

 

particle position update

function updateParticlePosition(particleMesh: THREE.InstancedMesh, positions:Float32Array){
  // positions 배열은 (x, y, z, w) 형식으로 particleCount * 4 길이만큼 있음.
  const particleCount = positions.length / 4; // 하나의 파티클당 4개의 값 (x, y, z, w)

  for (let i = 0; i < particleCount; i++) {
    // 각 파티클의 x, y, z 위치값 가져오기
    const x = positions[i * 4];
    const y = positions[i * 4 + 1];
    const z = positions[i * 4 + 2];

    // 파티클 위치에 맞는 매트릭스 설정
    const matrix = new THREE.Matrix4();
    const position = new THREE.Vector3(x, y, z);
    matrix.setPosition(position);

    // InstancedMesh에 행렬 적용
    particleMesh.setMatrixAt(i, matrix);
  }

  // InstancedMesh를 업데이트하여 변경 사항 반영
  particleMesh.instanceMatrix.needsUpdate = true;
}

파티클이 떨어지긴 떨어지는데, 하나로 뭉쳐져 버렸다.

이제보니 y값은 업데이트가 되는데, 되려 x,z 값은 0으로 초기화되어버렸다.

 

처음 값을 유지할 수 있도록 시작할 때의 position을 

position의 uniform data로 넣어주는 방법을 찾아보자.

 

particle 초기 위치 texture에 반영하기

this.initialPosition = this.gpuCompute.createTexture();
this.initialVelocity = this.gpuCompute.createTexture();
this.initialForce = this.gpuCompute.createTexture();
this.initParticlePosition();

this.positionVariable = this.gpuCompute.addVariable('positionTexture', computeIntegratePosition, this.initialPosition);
this.velocityVariable = this.gpuCompute.addVariable('velocityTexture', computeIntegrateVelocity, this.initialVelocity);
this.forceVariable = this.gpuCompute.addVariable('forceTexture', computeIntegrateForce, this.initialForce);

this.gpuCompute.setVariableDependencies(this.positionVariable, [this.positionVariable, this.velocityVariable]);
this.gpuCompute.setVariableDependencies(this.velocityVariable, [this.velocityVariable, this.forceVariable]);
this.gpuCompute.setVariableDependencies(this.forceVariable, [this.forceVariable]);

분명 여기서 initial position에 particle position을 반영하는 

initParticlePosition까지는 정상 작동이 되어서 image data가 잘 들어가 있음을 로그로 확인했다.

 

문제는 아마 shader로 넘어갔을 때 texture uv 좌표 계산 때문에 겹친 것일 것이다.

그래서 수정했다.

 

vec3 position = texture2D(positionTexture, vec2(gl_FragCoord.x / resolution.xy)).xyz;
vec3 velocity = texture2D(velocityTexture, vec2(gl_FragCoord.x / resolution.xy)).xyz;

원래 FragCoord에 resolution으로 나누지 않았다가 

resolution으로 나누어서 uv를 계산하라는 것을 보고 수정했다.

 

그랬더니 오잉?

이제 각 파티클들이 초기위치를 고려한 상태로 떨어지게 되었다.

 

particle damping

precision mediump float;

uniform float timestep;
uniform float particleMass;
uniform vec3 boxSize;
uniform float radius;
uniform float boundDamping;
uniform int particleLength;

// 첫 compute shader에서는 중복된 uniform
// uniform sampler2D velocityTexture;  // 속도를 입력으로 받음
// uniform sampler2D positionTexture;  // 이전 위치를 입력으로 받음

void main() {
    int id = int(gl_FragCoord.x);  // 각 픽셀은 파티클 ID와 매핑

    if (id >= particleLength) {
        gl_FragColor = vec4(0.0);  // 범위 밖일 경우 아무런 작업도 하지 않음
        return;
    }

    vec3 position = texture2D(positionTexture, vec2(gl_FragCoord.x / resolution.x)).xyz;
    vec3 velocity = texture2D(velocityTexture, vec2(gl_FragCoord.x / resolution.x)).xyz;

    vec3 newPosition = position + velocity * timestep;

    vec3 topRight = boxSize / 2.0;
    vec3 bottomLeft = -boxSize / 2.0;

    // Min Boundary Enforcements
    if (newPosition.x - radius < bottomLeft.x) {
        velocity.x *= boundDamping;
        newPosition.x = bottomLeft.x + radius;
    }
    if (newPosition.y - radius < bottomLeft.y) {
        velocity.y *= boundDamping;
        newPosition.y = bottomLeft.y + radius;
    }
    if (newPosition.z - radius < bottomLeft.z) {
        velocity.z *= boundDamping;
        newPosition.z = bottomLeft.z + radius;
    }

    // Max Boundary Enforcements
    if (newPosition.x + radius > topRight.x) {
        velocity.x *= boundDamping;
        newPosition.x = topRight.x - radius;
    }
    if (newPosition.y + radius > topRight.y) {
        velocity.y *= boundDamping;
        newPosition.y = topRight.y - radius;
    }
    if (newPosition.z + radius > topRight.z) {
        velocity.z *= boundDamping;
        newPosition.z = topRight.z - radius;
    }

    // 새로운 위치를 vec4로 저장
    gl_FragColor = vec4(newPosition, 1.0);
}

이건 position에 대해서 compute 하는 shader이다.

하지만 코드를 보면 boundary에 부딪혔을 때 velocity를 update하는 코드가 들어있다.

이게 여기가 아니라 velocity shader에 들어갔어야할 내용이다. 

 

엇...
    //#region velocity uniform
    this.velocityVariable.material.uniforms.particleMass = { value: 1.0 };
    this.velocityVariable.material.uniforms.particleLength = { value: this.totalParticles };
    // this.positionVariable.material.uniforms.viscosity = { value: 0.01 };
    // this.positionVariable.material.uniforms.gasConstant = { value: 1.4 };
    // this.positionVariable.material.uniforms.restDensity = { value: 1000.0 };
    this.positionVariable.material.uniforms.boundDamping = { value: this.boundDamping };
    // this.positionVariable.material.uniforms.pi = { value: Math.PI };
    this.positionVariable.material.uniforms.boxSize = { value: new THREE.Vector3(10, 10, 10) };
    
    this.positionVariable.material.uniforms.radius = { value: this.particleRadius };
    // this.positionVariable.material.uniforms.radius2 = { value: Math.pow(this.particleRadius, 2) };
    // this.positionVariable.material.uniforms.radius3 = { value: Math.pow(this.particleRadius, 3) };
    // this.positionVariable.material.uniforms.radius4 = { value: Math.pow(this.particleRadius, 4) };
    // this.positionVariable.material.uniforms.radius5 = { value: Math.pow(this.particleRadius, 5) };
    
    this.velocityVariable.material.uniforms.timestep = { value:  0.016 };
    //#endregion

velocity uniform을 설정하는 코드에 실수가 있었다.

상수들 일부를 positionVariable로 잘못 코드를 써놓은데 있었다. 

 

하지만 이걸 고쳐도 해결되지는 않았다;;

 

문제는 바닥에 부딪혀도 y축 velocity의 부호가 바뀌지 않고,

계속 음의 방향으로 증가한다는 것이다.

 

즉 충돌한 상태에서 반대로 속력이 튀지 않는다는 말이다. 

damping은 생각해보니 속력을 줄여주는 것이다. 하지만 여기서는 영역 밖으로 튀어나갈 때 속력의 방향을 반대로 바꿔주는 역할을 하는데.

 

코드를 보면서 하나 느끼는 점은.

shader의 순서가 position → velocity → force 순서대로 진행된다면,

new position이 경계 밖으로 나갔다가 다시 조정된 다음에 velocity shader를 수행하게 된다. 

 

다시 말하면, 이미 position이 조정된 상태에서 velocity shader로 가므로,

    if (position.y - radius < bottomLeft.y) {
        velocity.y *= boundDamping;
    }

velocity에서는 위 damping 코드가 작동하지 않을 것이라는 것이다. 

아니, 순서가 어떻게 되더라도 new position에 대해서 velocity shader가 알고 있어야한다.

 

precision mediump float;

uniform float timestep;
uniform float particleMass;
uniform vec3 boxSize;
uniform float radius;
uniform float boundDamping;
uniform int particleLength;

// uniform sampler2D velocityTexture;  // 이전 속도를 입력으로 받음
// uniform sampler2D forceTexture;     // 힘을 입력으로 받음

void main() {
    int id = int(gl_FragCoord.x);  // 각 픽셀은 파티클 ID와 매핑

    if (id >= particleLength) {
        gl_FragColor = vec4(0.0);  // 범위 밖일 경우 아무런 작업도 하지 않음
        return;
    }

    vec3 position = texture2D(positionTexture, vec2(gl_FragCoord.xy / resolution.xy)).xyz;
    vec3 velocity = texture2D(velocityTexture, vec2(gl_FragCoord.xy / resolution.xy)).xyz;
    vec3 force = texture2D(forceTexture, vec2(gl_FragCoord.xy / resolution.xy)).xyz;

    // 새로운 속도 계산
    vec3 newVelocity = velocity + ((force / particleMass) * timestep);
    vec3 newPosition = position + newVelocity * timestep;

    vec3 topRight = boxSize / 2.0;
    vec3 bottomLeft = -boxSize / 2.0;
    
    // Min Boundary Enforcements
    if (newPosition.x - radius < bottomLeft.x) {
        newVelocity.x *= boundDamping;
    }
    if (newPosition.y - radius < bottomLeft.y) {
        newVelocity.y *= boundDamping;
    }
    if (newPosition.z - radius < bottomLeft.z) {
        newVelocity.z *= boundDamping;
    }

    // Max Boundary Enforcements
    if (newPosition.x + radius > topRight.x) {
        newVelocity.x *= boundDamping;
    }
    if (newPosition.y + radius > topRight.y) {
        newVelocity.y *= boundDamping;
    }
    if (newPosition.z + radius > topRight.z) {
        newVelocity.z *= boundDamping;
    }

    // 속도를 vec4로 저장
    gl_FragColor = vec4(newVelocity, 1.0);
}

velocity shader에 new position을 업데이트하고 damping 조건에 반영하였다.

그리고 velocity 또한 damping을 통해 업데이트하니까 positoin이 boundary에 닿았을 때 튕겨나오는 로직이 정상 구현되었다.

 

particle integration 구현 

이렇게 파티클이 y방향으로 통 하고 튀는게 잘 구현되었다.

 

728x90
반응형