Last active
January 22, 2022 08:09
-
-
Save ashconnell/49a8c2c0fdde147e2c4cea0202c637c9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as THREE from 'three' | |
/** | |
* See: https://github.com/maximeq/three-js-capsule-geometry | |
*/ | |
export class CapsuleGeometry extends THREE.BufferGeometry { | |
constructor( | |
radiusTop, | |
radiusBottom, | |
height, | |
radialSegments, | |
heightSegments, | |
capsTopSegments, | |
capsBottomSegments, | |
thetaStart, | |
thetaLength | |
) { | |
super() | |
this.type = 'CapsuleGeometry' | |
this.parameters = { | |
radiusTop: radiusTop, | |
radiusBottom: radiusBottom, | |
height: height, | |
radialSegments: radialSegments, | |
heightSegments: heightSegments, | |
thetaStart: thetaStart, | |
thetaLength: thetaLength, | |
} | |
var scope = this | |
radiusTop = radiusTop !== undefined ? radiusTop : 1 | |
radiusBottom = radiusBottom !== undefined ? radiusBottom : 1 | |
height = height !== undefined ? height : 2 | |
radialSegments = Math.floor(radialSegments) || 8 | |
heightSegments = Math.floor(heightSegments) || 1 | |
capsTopSegments = Math.floor(capsTopSegments) || 2 | |
capsBottomSegments = Math.floor(capsBottomSegments) || 2 | |
thetaStart = thetaStart !== undefined ? thetaStart : 0.0 | |
thetaLength = thetaLength !== undefined ? thetaLength : 2.0 * Math.PI | |
// Alpha is the angle such that Math.PI/2 - alpha is the cone part angle. | |
var alpha = Math.acos((radiusBottom - radiusTop) / height) | |
var eqRadii = radiusTop - radiusBottom === 0 | |
var vertexCount = calculateVertexCount() | |
var indexCount = calculateIndexCount() | |
// buffers | |
var indices = new THREE.BufferAttribute( | |
new (indexCount > 65535 ? Uint32Array : Uint16Array)(indexCount), | |
1 | |
) | |
var vertices = new THREE.BufferAttribute( | |
new Float32Array(vertexCount * 3), | |
3 | |
) | |
var normals = new THREE.BufferAttribute( | |
new Float32Array(vertexCount * 3), | |
3 | |
) | |
var uvs = new THREE.BufferAttribute(new Float32Array(vertexCount * 2), 2) | |
// helper variables | |
var index = 0, | |
indexOffset = 0, | |
indexArray = [], | |
halfHeight = height / 2 | |
// generate geometry | |
generateTorso() | |
// build geometry | |
this.setIndex(indices) | |
this.addAttribute('position', vertices) | |
this.addAttribute('normal', normals) | |
this.addAttribute('uv', uvs) | |
// helper functions | |
function calculateVertexCount() { | |
var count = | |
(radialSegments + 1) * | |
(heightSegments + 1 + capsBottomSegments + capsTopSegments) | |
return count | |
} | |
function calculateIndexCount() { | |
var count = | |
radialSegments * | |
(heightSegments + capsBottomSegments + capsTopSegments) * | |
2 * | |
3 | |
return count | |
} | |
function generateTorso() { | |
var x, y | |
var normal = new THREE.Vector3() | |
var vertex = new THREE.Vector3() | |
var cosAlpha = Math.cos(alpha) | |
var sinAlpha = Math.sin(alpha) | |
var cone_length = new THREE.Vector2( | |
radiusTop * sinAlpha, | |
halfHeight + radiusTop * cosAlpha | |
) | |
.sub( | |
new THREE.Vector2( | |
radiusBottom * sinAlpha, | |
-halfHeight + radiusBottom * cosAlpha | |
) | |
) | |
.length() | |
// Total length for v texture coord | |
var vl = | |
radiusTop * alpha + cone_length + radiusBottom * (Math.PI / 2 - alpha) | |
var groupCount = 0 | |
// generate vertices, normals and uvs | |
var v = 0 | |
for (y = 0; y <= capsTopSegments; y++) { | |
var indexRow = [] | |
var a = Math.PI / 2 - alpha * (y / capsTopSegments) | |
v += (radiusTop * alpha) / capsTopSegments | |
var cosA = Math.cos(a) | |
var sinA = Math.sin(a) | |
// calculate the radius of the current row | |
var radius = cosA * radiusTop | |
for (x = 0; x <= radialSegments; x++) { | |
var u = x / radialSegments | |
var theta = u * thetaLength + thetaStart | |
var sinTheta = Math.sin(theta) | |
var cosTheta = Math.cos(theta) | |
// vertex | |
vertex.x = radius * sinTheta | |
vertex.y = halfHeight + sinA * radiusTop | |
vertex.z = radius * cosTheta | |
vertices.setXYZ(index, vertex.x, vertex.y, vertex.z) | |
// normal | |
normal.set(cosA * sinTheta, sinA, cosA * cosTheta) | |
normals.setXYZ(index, normal.x, normal.y, normal.z) | |
// uv | |
uvs.setXY(index, u, 1 - v / vl) | |
// save index of vertex in respective row | |
indexRow.push(index) | |
// increase index | |
index++ | |
} | |
// now save vertices of the row in our index array | |
indexArray.push(indexRow) | |
} | |
var cone_height = height + cosAlpha * radiusTop - cosAlpha * radiusBottom | |
var slope = (sinAlpha * (radiusBottom - radiusTop)) / cone_height | |
for (y = 1; y <= heightSegments; y++) { | |
var indexRow = [] | |
v += cone_length / heightSegments | |
// calculate the radius of the current row | |
var radius = | |
sinAlpha * | |
((y * (radiusBottom - radiusTop)) / heightSegments + radiusTop) | |
for (x = 0; x <= radialSegments; x++) { | |
var u = x / radialSegments | |
var theta = u * thetaLength + thetaStart | |
var sinTheta = Math.sin(theta) | |
var cosTheta = Math.cos(theta) | |
// vertex | |
vertex.x = radius * sinTheta | |
vertex.y = | |
halfHeight + | |
cosAlpha * radiusTop - | |
(y * cone_height) / heightSegments | |
vertex.z = radius * cosTheta | |
vertices.setXYZ(index, vertex.x, vertex.y, vertex.z) | |
// normal | |
normal.set(sinTheta, slope, cosTheta).normalize() | |
normals.setXYZ(index, normal.x, normal.y, normal.z) | |
// uv | |
uvs.setXY(index, u, 1 - v / vl) | |
// save index of vertex in respective row | |
indexRow.push(index) | |
// increase index | |
index++ | |
} | |
// now save vertices of the row in our index array | |
indexArray.push(indexRow) | |
} | |
for (y = 1; y <= capsBottomSegments; y++) { | |
var indexRow = [] | |
var a = | |
Math.PI / 2 - alpha - (Math.PI - alpha) * (y / capsBottomSegments) | |
v += (radiusBottom * alpha) / capsBottomSegments | |
var cosA = Math.cos(a) | |
var sinA = Math.sin(a) | |
// calculate the radius of the current row | |
var radius = cosA * radiusBottom | |
for (x = 0; x <= radialSegments; x++) { | |
var u = x / radialSegments | |
var theta = u * thetaLength + thetaStart | |
var sinTheta = Math.sin(theta) | |
var cosTheta = Math.cos(theta) | |
// vertex | |
vertex.x = radius * sinTheta | |
vertex.y = -halfHeight + sinA * radiusBottom | |
vertex.z = radius * cosTheta | |
vertices.setXYZ(index, vertex.x, vertex.y, vertex.z) | |
// normal | |
normal.set(cosA * sinTheta, sinA, cosA * cosTheta) | |
normals.setXYZ(index, normal.x, normal.y, normal.z) | |
// uv | |
uvs.setXY(index, u, 1 - v / vl) | |
// save index of vertex in respective row | |
indexRow.push(index) | |
// increase index | |
index++ | |
} | |
// now save vertices of the row in our index array | |
indexArray.push(indexRow) | |
} | |
// generate indices | |
for (x = 0; x < radialSegments; x++) { | |
for ( | |
y = 0; | |
y < capsTopSegments + heightSegments + capsBottomSegments; | |
y++ | |
) { | |
// we use the index array to access the correct indices | |
var i1 = indexArray[y][x] | |
var i2 = indexArray[y + 1][x] | |
var i3 = indexArray[y + 1][x + 1] | |
var i4 = indexArray[y][x + 1] | |
// face one | |
indices.setX(indexOffset, i1) | |
indexOffset++ | |
indices.setX(indexOffset, i2) | |
indexOffset++ | |
indices.setX(indexOffset, i4) | |
indexOffset++ | |
// face two | |
indices.setX(indexOffset, i2) | |
indexOffset++ | |
indices.setX(indexOffset, i3) | |
indexOffset++ | |
indices.setX(indexOffset, i4) | |
indexOffset++ | |
} | |
} | |
} | |
} | |
fromPoints( | |
pointA, | |
pointB, | |
radiusA, | |
radiusB, | |
radialSegments, | |
heightSegments, | |
capsTopSegments, | |
capsBottomSegments, | |
thetaStart, | |
thetaLength | |
) { | |
let cmin = null | |
let cmax = null | |
let rmin = null | |
let rmax = null | |
if (radiusA > radiusB) { | |
cmax = pointA | |
cmin = pointB | |
rmax = radiusA | |
rmin = radiusB | |
} else { | |
cmax = pointA | |
cmin = pointB | |
rmax = radiusA | |
rmin = radiusB | |
} | |
const c0 = cmin | |
const c1 = cmax | |
const r0 = rmin | |
const r1 = rmax | |
const sphereCenterTop = new THREE.Vector3(c0.x, c0.y, c0.z) | |
const sphereCenterBottom = new THREE.Vector3(c1.x, c1.y, c1.z) | |
const radiusTop = r0 | |
const radiusBottom = r1 | |
let height = sphereCenterTop.distanceTo(sphereCenterBottom) | |
// If the big sphere contains the small one, return a SphereBufferGeometry | |
if (height < Math.abs(r0 - r1)) { | |
let g = new THREE.SphereBufferGeometry( | |
r1, | |
radialSegments, | |
capsBottomSegments, | |
thetaStart, | |
thetaLength | |
) | |
g.translate(r1.x, r1.y, r1.z) | |
return g | |
} | |
// useful values | |
const alpha = Math.acos((radiusBottom - radiusTop) / height) | |
const cosAlpha = Math.cos(alpha) | |
const sinAlpha = Math.sin(alpha) | |
// compute cylinder properties | |
const coneHeight = height + cosAlpha * radiusTop - cosAlpha * radiusBottom | |
const cylTopRadius = sinAlpha * radiusTop | |
const cylBottomRadius = sinAlpha * radiusBottom | |
// compute rotation matrix | |
const rotationMatrix = new THREE.Matrix4() | |
const quaternion = new THREE.Quaternion() | |
const capsuleModelUnitVector = new THREE.Vector3(0, 1, 0) | |
const capsuleUnitVector = new THREE.Vector3() | |
capsuleUnitVector.subVectors(sphereCenterTop, sphereCenterBottom) | |
capsuleUnitVector.normalize() | |
quaternion.setFromUnitVectors(capsuleModelUnitVector, capsuleUnitVector) | |
rotationMatrix.makeRotationFromQuaternion(quaternion) | |
// compute translation matrix from center point | |
const translationMatrix = new THREE.Matrix4() | |
const cylVec = new THREE.Vector3() | |
cylVec.subVectors(sphereCenterTop, sphereCenterBottom) | |
cylVec.normalize() | |
let cylTopPoint = new THREE.Vector3() | |
cylTopPoint = sphereCenterTop | |
cylTopPoint.addScaledVector(cylVec, cosAlpha * radiusTop) | |
let cylBottomPoint = new THREE.Vector3() | |
cylBottomPoint = sphereCenterBottom | |
cylBottomPoint.addScaledVector(cylVec, cosAlpha * radiusBottom) | |
// computing lerp for color | |
const dir = new THREE.Vector3() | |
dir.subVectors(cylBottomPoint, cylTopPoint) | |
dir.normalize() | |
const middlePoint = new THREE.Vector3() | |
middlePoint.lerpVectors(cylBottomPoint, cylTopPoint, 0.5) | |
translationMatrix.makeTranslation( | |
middlePoint.x, | |
middlePoint.y, | |
middlePoint.z | |
) | |
// Instanciate a CylinderBufferGeometry from three.js | |
let g = new CapsuleGeometry( | |
radiusBottom, | |
radiusTop, | |
height, | |
radialSegments, | |
heightSegments, | |
capsTopSegments, | |
capsBottomSegments, | |
thetaStart, | |
thetaLength | |
) | |
// applying transformations | |
g.applyMatrix(rotationMatrix) | |
g.applyMatrix(translationMatrix) | |
return g | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const geometry = new CapsuleGeometry( | |
0.25, | |
0.25, | |
1.4, | |
8, | |
1, | |
4, | |
4 | |
) | |
const material = new THREE.MeshStandardMaterial({ color: 'white' }) | |
const capsule = new THREE.Mesh(geometry, material) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment