Transform Controls gizmo(gui) 끄기
transform controls gizmo가 뜬 상태에서 다른 오브젝트를 클릭하거나, 모드가 변경될 때 gizmo가 꺼지도록 하자.
주의할 것은 object를 바꾼 경우와 controler를 클릭한 경우를 구분해야한다.
window.addEventListener('mousedown', ()=>{
switch(mode.curMode){
case "TRANSFORM":
const object = getIntersectObject(scene, camera)
const transformControlsObject = getIntersectTransformControls(scene, camera)
if (object) {
transformControls.attach(object);
}
else if(object && transformControlsObject){
// nothing
console.log(`controler`)
}
else{
transformControls.detach();
console.log(`detach`)
}
break;
}
}, false)
이런 식으로 transfrom controls를 따로 검출하려고 했지만, 허공을 클릭해도 controler가 클릭되는 현상이 있다.
이게 transform contols가 투명한 panel object를 가지고 있는 모양이라, transform controls중 특히
이런식으로 hover 이벤트가 뜨는 경우만을 검출해야한다.
라고 생각해서 봤는데 dragging을 확인할 수 있는 값이 있어서 사용해보았다.
https://threejs.org/docs/#examples/en/controls/TransformControls
이렇게 object를 감지하지 못해지만 controler를 dragging하는 중인 경우에는 아무것도 취하지 않도록 설정했고,
test해보니 잘 되는 것을 볼 수 있다.
mesh attacher constraint geometry merge
merge geometry를 하면 시뮬레이션 성능이 상당히 느려지는 상황이다.
이를 어떻게 해결하면 좋을지.. 아직 좀 더 고찰해보아야하지만 distance constraint로 attach하는 기능에 mesh merge case도 구현하고 실행해보자.
여기까지 하고 나면 초기에 구상했던 기능들은 모두 들어가는 것이다.
이제 옷을 시착하는 시뮬레이션을 위해 정적인 오브젝트와 cloth 객체를 충돌하도록 하는 구도로 개발을 진행하려고 한다.
constraint를 수정하는 방법에도 merge geometry case를 추가했다.
merge를 하면 현재 성능은 저하되지만 시뮬레이션 자체는 잘 진행되는지 확인해보자.
마찬가지로 시뮬레이션 속도가 느려서 배속한 결과이다.
merge 되면서 연결한 두 정점이 가까워지는게 보인다.
연결한 두 정점을 기준으로 두 mesh가 합쳐졌다.
mesh separate on/off 기능 구현하기
mesh가 분리되는 기능의 성능 저하를 가진 채로, 모델에 피팅하는 시뮬레이션을 돌리기에는 적합하지 않다.
그래서 remove_vertex와 remove_edge에서 mesh separate 기능을 custom하게 on/off 할 수 있도록 gui에 기능을 추가하자.
separate mesh를 on/off 해서 mesh를 분리할 것인지 하지 않을 것인지를 선택 가능하게 하였다.
지금 옷 모델을 두 번 분리한 상태인데
시뮬레이션 해보면 mesh로 분리한 쪽은 고정된 vertex에 인해 펄럭이고,
mesh로 분리하지 않고 geometry만 분리된 조각은 날아가고 있다.
static object cloth collision 구현
cloth simulation의 마지막은 피팅이다. 사람 아바타에게 옷을 입히는 것 같은 시뮬레이션을 하는 것이 종착점이라고 할 수 있는데, 현재 프로젝트에서는 정적 오브젝트와 cloth 객체와의 충돌 연산이 되어야 피팅이 가능할 것 같다.
PBD를 통해서 정적 오브젝트와 충돌하는 방법은 굉장히 많은데, 문제는 현재 구현된 방향이 외부 충돌에 대한 고려를 하지 않고 구현된 코드를 참고하여 만들었다는 점이다.
그래서 외부 충돌의 경우 floor collision과 비슷한 방식으로 구현하려고 한다.
새로운 오브젝트의 geometry가 있으면 particle의 current position이 오브젝트 안쪽으로 들어간 경우
반대 방향으로 밀어주는 로직을 사용해보려고 한다.
일단 particle이 있으면, 해당 particle과 가장 가까운 object의 face를 찾은 후, 해당 face의 normal vector를 이용해서 face를 통과하였는지와 통과하였다면 거리가 얼마인지를 구할 수 있을 것이다.
그 후 통과한 거리만큼 normal 방향으로 밀어서 충돌 보정을 해주는 방식을 구상할 수 있다.
아니면 다른 방법으로는 충돌에 한해서 물리엔진을 이용하는 방법도 생각해보면 좋을 듯 하다.
threejs 물리엔진에 대해
https://discourse.threejs.org/t/selecting-physics-engine/60344
사실 처음부터 cloth simulation 을 물리엔진으로 사용할 수 있다, 하지만 프로젝트는 PBD simulation을 기반으로 구현해보고 geometry를 편집하면서 시뮬레이션에 적용하는 기능을 만들어보는 것이 목표이기 때문에 직접 구현하였다.
static object mesh collision : face 기준 충돌방식의 오류
충돌의 경우 직접 구현하면 제대로 동작할지가 의문이지만, 한 번 해보자
import { Mesh } from "three";
import ClothSelfCollision, { Collision } from "./collision";
import { Constraint, ConstraintFactory } from "./constraint";
import { Hash } from "./hash";
import {
vecAdd,
vecCopy,
vecDot,
vecLengthSquared,
vecScale,
vecSetCross,
vecSetDiff,
vecSetZero,
} from "./math";
let height = -0.7
/**
* Abstract class that all meshes should inherit from to use XPBD physics
*/
export default abstract class PhysicsObject {
numParticles: number;
positions: Float32Array;
prevPositions: Float32Array;
vels: Float32Array;
invMass: Float32Array;
normals: Float32Array;
indices: Uint16Array;
neighbors: Float32Array;
constraints: Constraint[];
constraintFactory: ConstraintFactory;
collisions: Collision[];
constructor(mesh: Mesh) {
this.numParticles = mesh.geometry.attributes.position.count;
this.positions = new Float32Array(mesh.geometry.attributes.position.array);
this.normals = new Float32Array(mesh.geometry.attributes.normal.array);
this.prevPositions = new Float32Array(mesh.geometry.attributes.position.array);
this.vels = new Float32Array(3 * this.numParticles);
this.invMass = new Float32Array(this.numParticles);
this.indices = new Uint16Array(mesh.geometry.index?.array ?? new Array(0));
this.constraints = [];
this.collisions = [];
this.invMass = this.initInvMass();
this.neighbors = this.findTriNeighbors();
this.constraintFactory = new ConstraintFactory(
this.positions,
this.invMass,
this.indices,
this.neighbors,
null
);
}
setFloorHeight(_height:number){
height = _height
}
solve(dt: number, collisionMesh: Mesh) {
for (let i = 0; i < this.numParticles; i++) {
// Floor collision ( we currently don't have a need for it)
let y = this.positions[3 * i + 1];
if (y < height) {
vecCopy(this.positions, i, this.prevPositions, i);
this.positions[3 * i + 1] = height;
}
// 충돌 처리 추가
this.handleCollision(i, collisionMesh);
}
for (const constraint of this.constraints) {
constraint.solve(dt);
}
for (const collision of this.collisions) {
collision.solve(dt);
}
}
preSolve(dt: number, gravity: Float32Array) {
for (let i = 0; i < this.numParticles; i++) {
if (this.invMass[i] == 0.0) continue;
vecAdd(this.vels, i, gravity, 0, dt);
const v = Math.sqrt(vecLengthSquared(this.vels, i));
const maxV = 0.2 * (0.01 / dt);
if (v > maxV) {
vecScale(this.vels, i, maxV / v);
}
vecCopy(this.prevPositions, i, this.positions, i);
vecAdd(this.positions, i, this.vels, i, dt);
}
}
postSolve(dt: number) {
for (let i = 0; i < this.numParticles; i++) {
if (this.invMass[i] == 0.0) continue;
vecSetDiff(
this.vels,
i,
this.positions,
i,
this.prevPositions,
i,
1.0 / dt
);
}
}
updateVertexNormals() {
for (let i = 0; i < this.numParticles; i++) {
vecSetZero(this.normals, i);
}
for (let i = 0; i < this.numParticles; i++) {
const id0 = this.indices[3 * i];
const id1 = this.indices[3 * i + 1];
const id2 = this.indices[3 * i + 2];
const e0 = [0, 0, 0];
const e1 = [0, 0, 0];
const c = [0, 0, 0];
// Find Area of Triangle
// Calculate edge vectors from id0
vecSetDiff(e0, 0, this.positions, id1, this.positions, id0);
vecSetDiff(e1, 0, this.positions, id2, this.positions, id0);
// Area of triangle 1/2 |AB x AC|
vecSetCross(c, 0, e0, 0, e1, 0);
vecAdd(this.normals, id0, c, 0, 0.333);
vecAdd(this.normals, id1, c, 0, 0.333);
vecAdd(this.normals, id2, c, 0, 0.333);
}
}
protected updatePhysics(){
this.invMass = this.initInvMass();
this.neighbors = this.findTriNeighbors();
}
private initInvMass(): Float32Array {
const invMass = new Float32Array(this.numParticles);
const numTris = this.indices.length / 3;
const e0 = [0.0, 0.0, 0.0]; // edge 0 vector
const e1 = [0.0, 0.0, 0.0]; // edge 1 vector
const c = [0.0, 0.0, 0.0]; // cross vector of e0 x e1
for (let i = 0; i < numTris; i++) {
const id0 = this.indices[3 * i];
const id1 = this.indices[3 * i + 1];
const id2 = this.indices[3 * i + 2];
// Find Area of Triangle
// Calculate edge vectors from id0
vecSetDiff(e0, 0, this.positions, id1, this.positions, id0);
vecSetDiff(e1, 0, this.positions, id2, this.positions, id0);
// Area of triangle 1/2 |AB x AC|
vecSetCross(c, 0, e0, 0, e1, 0);
const A = 0.5 * Math.sqrt(vecLengthSquared(c, 0)); // magnitude of cross vector
// Divide mass among 3 points in triangle
const pInvMass = A > 0.0 ? 1.0 / A / 3.0 : 0.0;
// Add since vertices may be shared
invMass[id0] += pInvMass;
invMass[id1] += pInvMass;
invMass[id2] += pInvMass;
}
return invMass;
}
private findTriNeighbors(): Float32Array {
const edges = [];
const numTris = this.indices.length / 3;
for (let i = 0; i < numTris; i++) {
for (let j = 0; j < 3; j++) {
const id0 = this.indices[3 * i + j];
const id1 = this.indices[3 * i + ((j + 1) % 3)];
edges.push({
id0: Math.min(id0, id1), // particle 1
id1: Math.max(id0, id1), // particle 2
edgeNr: 3 * i + j, // global edge number
});
}
}
// sort so common edges are next to each other
edges.sort((a, b) =>
a.id0 < b.id0 || (a.id0 == b.id0 && a.id1 < b.id1) ? -1 : 1
);
// find matching edges
const neighbors = new Float32Array(3 * numTris);
neighbors.fill(-1); // -1 means open edge, as in no neighbors
let i = 0;
while (i < edges.length) {
const e0 = edges[i];
const e1 = edges[i + 1];
// catch exception: if edges length is odd
if(edges.length <= i+1) {
i+=2
continue
}
// If the particles share the same edge, update the neighbors list
// with their neighbors corresponding global edge number
if (e0.id0 === e1.id0 && e0.id1 === e1.id1) {
neighbors[e0.edgeNr] = e1.edgeNr;
neighbors[e1.edgeNr] = e0.edgeNr;
}
i += 2;
}
return neighbors;
}
private handleCollision(particleIndex: number, collisionMesh: Mesh) {
// particle 위치
const particlePos = [
this.positions[3 * particleIndex],
this.positions[3 * particleIndex + 1],
this.positions[3 * particleIndex + 2]
];
// collisionMesh의 geometry 정보를 가져옴
const collisionPositions = collisionMesh.geometry.attributes.position.array;
const collisionIndices = collisionMesh.geometry.index!.array;
let closestDistance = Infinity;
let closestNormal = [0, 0, 0];
let closestPoint = [0, 0, 0];
for (let i = 0; i < collisionIndices.length; i += 3) {
const id0 = collisionIndices[i];
const id1 = collisionIndices[i + 1];
const id2 = collisionIndices[i + 2];
const v0 = [
collisionPositions[3 * id0],
collisionPositions[3 * id0 + 1],
collisionPositions[3 * id0 + 2]
];
const v1 = [
collisionPositions[3 * id1],
collisionPositions[3 * id1 + 1],
collisionPositions[3 * id1 + 2]
];
const v2 = [
collisionPositions[3 * id2],
collisionPositions[3 * id2 + 1],
collisionPositions[3 * id2 + 2]
];
// face의 법선 벡터 계산
const normal = this.computeFaceNormal(v0, v1, v2);
// particle과 face 간의 거리 계산
const [distance, pointOnFace] = this.computeDistanceToFace(particlePos, v0, v1, v2, normal);
if (distance < closestDistance) {
closestDistance = distance;
vecCopy(closestPoint, 0, pointOnFace, 0);
vecCopy(closestNormal, 0, normal, 0);
}
}
// 만약 파티클이 face를 통과했다면 충돌 보정
if (closestDistance < 0) {
vecAdd(this.positions, particleIndex, closestNormal, 0, -closestDistance * 0.001);
}
}
private computeFaceNormal(v0: number[], v1: number[], v2: number[]): number[] {
const e0 = [0, 0, 0];
const e1 = [0, 0, 0];
const normal = [0, 0, 0];
vecSetDiff(e0, 0, v1, 0, v0, 0);
vecSetDiff(e1, 0, v2, 0, v0, 0);
vecSetCross(normal, 0, e0, 0, e1, 0);
const length = Math.sqrt(vecLengthSquared(normal, 0));
if (length > 0) {
vecScale(normal, 0, 1 / length);
}
return normal;
}
private computeDistanceToFace(p: number[], v0: number[], v1: number[], v2: number[], normal: number[]): [number, number[]] {
const toParticle = [0, 0, 0];
vecSetDiff(toParticle, 0, p, 0, v0, 0);
const distance = vecDot(toParticle, 0, normal, 0);
const projectedPoint = [0, 0, 0];
vecCopy(projectedPoint, 0, p, 0);
vecAdd(projectedPoint, 0, normal, 0, -distance);
// Check if the projected point is within the triangle
if (this.isPointInTriangle(projectedPoint, v0, v1, v2)) {
return [distance, projectedPoint];
} else {
// If not, find the closest point on the triangle to the projected point
const closestPoint = this.closestPointOnTriangle(projectedPoint, v0, v1, v2);
const closestVector = [0, 0, 0];
vecSetDiff(closestVector, 0, p, 0, closestPoint, 0);
const closestDistance = vecDot(closestVector, 0, normal, 0);
return [closestDistance, closestPoint];
}
}
/**
* 삼각형 내부에 포인트가 있는지 확인하는 함수
* @param p 확인할 포인트
* @param v0 삼각형의 첫 번째 정점
* @param v1 삼각형의 두 번째 정점
* @param v2 삼각형의 세 번째 정점
* @returns 삼각형 내부에 있다면 true, 그렇지 않으면 false
*/
private isPointInTriangle(p: number[], v0: number[], v1: number[], v2: number[]): boolean {
const e0 = [0, 0, 0];
const e1 = [0, 0, 0];
const e2 = [0, 0, 0];
vecSetDiff(e0, 0, v1, 0, v0, 0);
vecSetDiff(e1, 0, v2, 0, v1, 0);
vecSetDiff(e2, 0, v0, 0, v2, 0);
const c0 = [0, 0, 0];
const c1 = [0, 0, 0];
const c2 = [0, 0, 0];
vecSetDiff(c0, 0, p, 0, v0, 0);
vecSetDiff(c1, 0, p, 0, v1, 0);
vecSetDiff(c2, 0, p, 0, v2, 0);
const cross0 = [0, 0, 0];
const cross1 = [0, 0, 0];
const cross2 = [0, 0, 0];
vecSetCross(cross0, 0, e0, 0, c0, 0);
vecSetCross(cross1, 0, e1, 0, c1, 0);
vecSetCross(cross2, 0, e2, 0, c2, 0);
if (vecDot(cross0, 0, cross1, 0) >= 0 && vecDot(cross1, 0, cross2, 0) >= 0 && vecDot(cross2, 0, cross0, 0) >= 0) {
return true;
}
return false;
}
/**
* 삼각형 외부에 있는 경우 삼각형의 가장 가까운 점을 찾는 함수
* @param p 확인할 포인트
* @param v0 삼각형의 첫 번째 정점
* @param v1 삼각형의 두 번째 정점
* @param v2 삼각형의 세 번째 정점
* @returns 삼각형에서 가장 가까운 점
*/
private closestPointOnTriangle(p: number[], v0: number[], v1: number[], v2: number[]): number[] {
const ab = [0, 0, 0];
const ac = [0, 0, 0];
const ap = [0, 0, 0];
vecSetDiff(ab, 0, v1, 0, v0, 0);
vecSetDiff(ac, 0, v2, 0, v0, 0);
vecSetDiff(ap, 0, p, 0, v0, 0);
const d1 = vecDot(ab, 0, ap, 0);
const d2 = vecDot(ac, 0, ap, 0);
if (d1 <= 0.0 && d2 <= 0.0) {
return v0; // barycentric coordinates (1,0,0)
}
const bp = [0, 0, 0];
vecSetDiff(bp, 0, p, 0, v1, 0);
const d3 = vecDot(ab, 0, bp, 0);
const d4 = vecDot(ac, 0, bp, 0);
if (d3 >= 0.0 && d4 <= d3) {
return v1; // barycentric coordinates (0,1,0)
}
const vc = d1 * d4 - d3 * d2;
if (vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0) {
const v = d1 / (d1 - d3);
const result = [0, 0, 0];
vecSetDiff(result, 0, v1, 0, v0, 0);
vecScale(result, 0, v);
vecAdd(result, 0, v0, 0);
return result; // barycentric coordinates (1-v, v, 0)
}
const cp = [0, 0, 0];
vecSetDiff(cp, 0, p, 0, v2, 0);
const d5 = vecDot(ab, 0, cp, 0);
const d6 = vecDot(ac, 0, cp, 0);
if (d6 >= 0.0 && d5 <= d6) {
return v2; // barycentric coordinates (0,0,1)
}
const vb = d5 * d2 - d1 * d6;
if (vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0) {
const w = d2 / (d2 - d6);
const result = [0, 0, 0];
vecSetDiff(result, 0, v2, 0, v0, 0);
vecScale(result, 0, w);
vecAdd(result, 0, v0, 0);
return result; // barycentric coordinates (1-w, 0, w)
}
const va = d3 * d6 - d5 * d4;
if (va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0) {
const w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
const result = [0, 0, 0];
vecSetDiff(result, 0, v2, 0, v1, 0);
vecScale(result, 0, w);
vecAdd(result, 0, v1, 0);
return result; // barycentric coordinates (0, 1-w, w)
}
const denom = 1.0 / (va + vb + vc);
const v = vb * denom;
const w = vc * denom;
const result = [0, 0, 0];
vecSetDiff(result, 0, v1, 0, v0, 0);
vecScale(result, 0, v);
vecAdd(result, 0, v0, 0);
vecSetDiff(result, 0, v2, 0, v0, 0);
vecScale(result, 0, w);
vecAdd(result, 0, v0, 0);
return result; // barycentric coordinates (1-v-w, v, w)
}
}
export class ClothPhysicsObject extends PhysicsObject {
thickness: number;
hash: Hash;
constructor(mesh: Mesh, thickness: number) {
super(mesh);
this.thickness = thickness;
// Spacing calculated by looking into the obj file and seeing the length between two particles.
const spacing = thickness;
this.hash = new Hash(spacing, this.numParticles);
}
preIntegration(dt: number) {
this.hash.create(this.positions);
this.hash.queryAll(this.positions, ((1 / 60) * 0.2 * this.thickness) / dt);
}
/**
* Adds a DistanceConstraint to the Cloth physics object
* @param compliance
*/
public registerDistanceConstraint(compliance: number) {
this.constraints.push(
this.constraintFactory.createDistanceConstraint(compliance)
);
}
/**
* Adds a PerformantBendingConstraint to the Cloth physics object
* @param compliance
*/
public registerPerformantBendingConstraint(compliance: number) {
this.constraints.push(
this.constraintFactory.createPerformantBendingConstraint(compliance)
);
}
/**
* Adds an IsometricBendingConstraint to the Cloth physics object
* @param compliance
*/
public registerIsometricBendingConstraint(compliance: number) {
this.constraints.push(
this.constraintFactory.createIsometricBendingConstraint(compliance)
);
}
/**
* Adds a Self Collision constraint to the Cloth physics object
*/
public registerSelfCollision() {
this.collisions.push(
new ClothSelfCollision(
this.positions,
this.prevPositions,
this.invMass,
this.thickness,
this.hash
)
);
}
}
floor collision이 있는 solve 함수 안에서 위에서 언급한 방식대로 구현을 해보았는데, 연산량이 너무 많은 것 같기도 하고,
뭔가 방법이 잘못된 것 같다.
box mesh를 테스트용으로 하나 놓았는데, 충돌하는게 아니라 오히려 빨려 들어가고 있다.
아무래도 충돌하여 mesh내부로 들어가는 체크를 하는게, 반대편 face의 측면에서는 뚫고 들어갔다고 인지를 해서 계속해서 힘을 받고 있는 것이라고 예측된다.
그러니까 닫힌 mesh라고 한다면, face를 기준으로 충돌처리를 하면 모든 face normal방향에서 힘을 받는 이상한 현상이 발생하는 것이다.
그럼 충돌처리를 어떻게 해야할까. 아무래도 AABB와 같은 알고리즘을 통한 bounding box로직으로 충돌을 체크하는 방법이 맞다고 생각한다.
bouding box 로직으로 충돌을 체크하는 방법은 사실 일반적인 물리엔진에 들어있는 방식이다.
이번에는 물리엔진의 힘을 빌려 bounding box collision 방법을 채용해오자..
물리엔진 사용하기
npm install --save cannon
npm install --save @types/cannon
cannon.js와 cannon types를 설치한다.
이것 저것 시도중 ...
여러모로 시도를 해보았지만, 구조적인 한계가 있었다.
물리엔진으로 응용할 수 있는 한계가 뚜렷하게 존재하기 때문에, 완전히 collision을 이루는 방법은 어렵다.
다른 방법으로 충돌 감지시 일정 수준으로 반대로 밀어내는 방법은 있었다.
일단 걸리적 거리는 바람과 고정 기능을 제외시키자.
시뮬레이션 바람 기능 제거
아... gravity z방향으로 cos으로 하고있는게 여기있었다..
한참 찾고있었는데..
일단 제거했고, 이 상태로 떨어뜨려서 반대 반향으로 미는 힘을 늘려보면
이렇게 눕혀서 떨어뜨렸을 때 부들거리면서 밀려나고 내려오고의 반복이 된다.
정황상 의도에 맞게 좀 수정을 해보고 싶은데.
이렇게 했을 때 잘 안된다...
우여곡절..
모델 추가 및 테스트
마네킹 모델을 추가하고, 결국 collision의 경우 solve 함수 안에서 밀어내는 방식으로 구현했다.