three.js GPGPU SPH - particle integration 구현하기
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방향으로 통 하고 튀는게 잘 구현되었다.