Skip to content

Commit

Permalink
feat: add CurveModifier
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuaellis authored and giulioz committed Mar 1, 2021
1 parent 2fc77a5 commit 8882d72
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 57 deletions.
155 changes: 98 additions & 57 deletions src/modifiers/CurveModifier.js → src/modifiers/CurveModifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,25 @@ import {
NearestFilter,
DynamicDrawUsage,
Matrix4,
Material,
Shader,
Curve,
BufferGeometry,
} from 'three'

import { TUniform } from 'types/helpers'

/**
* Make a new DataTexture to store the descriptions of the curves.
*
* @param { number } numberOfCurves the number of curves needed to be described by this texture.
*/
export function initSplineTexture(numberOfCurves = 1) {
export const initSplineTexture = (numberOfCurves = 1): DataTexture => {
const dataArray = new Float32Array(TEXTURE_WIDTH * TEXTURE_HEIGHT * numberOfCurves * BITS)
const dataTexture = new DataTexture(dataArray, TEXTURE_WIDTH, TEXTURE_HEIGHT * numberOfCurves, RGBFormat, FloatType)

dataTexture.wrapS = RepeatWrapping
dataTexture.wrapY = RepeatWrapping
dataTexture.wrapT = RepeatWrapping
dataTexture.magFilter = NearestFilter
dataTexture.needsUpdate = true

Expand All @@ -39,7 +45,11 @@ export function initSplineTexture(numberOfCurves = 1) {
* @param { Curve } splineCurve The curve to describe
* @param { number } offset Which curve slot to write to
*/
export function updateSplineTexture(texture, splineCurve, offset = 0) {
export const updateSplineTexture = <TCurve extends Curve<any>>(
texture: DataTexture,
splineCurve: TCurve,
offset = 0,
): void => {
const numberOfPoints = Math.floor(TEXTURE_WIDTH * (TEXTURE_HEIGHT / 4))
splineCurve.arcLengthDivisions = numberOfPoints / 2
splineCurve.updateArcLengths()
Expand All @@ -63,7 +73,7 @@ export function updateSplineTexture(texture, splineCurve, offset = 0) {
texture.needsUpdate = true
}

function setTextureValue(texture, index, x, y, z, o) {
const setTextureValue = (texture: DataTexture, index: number, x: number, y: number, z: number, o: number): void => {
const image = texture.image
const { data } = image
const i = BITS * TEXTURE_WIDTH * o // Row Offset
Expand All @@ -72,12 +82,21 @@ function setTextureValue(texture, index, x, y, z, o) {
data[index * BITS + i + 2] = z
}

export interface CurveModifierUniforms {
spineTexture: TUniform<DataTexture>
pathOffset: TUniform<number>
pathSegment: TUniform<number>
spineOffset: TUniform<number>
spineLength: TUniform<number>
flow: TUniform<number>
}

/**
* Create a new set of uniforms for describing the curve modifier
*
* @param { DataTexture } Texture which holds the curve description
*/
export function getUniforms(splineTexture) {
export function getUniforms(splineTexture: DataTexture): CurveModifierUniforms {
const uniforms = {
spineTexture: { value: splineTexture },
pathOffset: { type: 'f', value: 0 }, // time of path curve
Expand All @@ -89,17 +108,25 @@ export function getUniforms(splineTexture) {
return uniforms
}

export function modifyShader(material, uniforms, numberOfCurves = 1) {
export type ModifiedMaterial<TMaterial extends Material> = TMaterial & {
__ok: boolean
}

export function modifyShader<TMaterial extends Material = Material>(
material: ModifiedMaterial<TMaterial>,
uniforms: CurveModifierUniforms,
numberOfCurves = 1,
) {
if (material.__ok) return
material.__ok = true

material.onBeforeCompile = (shader) => {
material.onBeforeCompile = (shader: Shader & { __modified: boolean }) => {
if (shader.__modified) return
shader.__modified = true

Object.assign(shader.uniforms, uniforms)

const vertexShader = `
const vertexShader = /* glsl */ `
uniform sampler2D spineTexture;
uniform float pathOffset;
uniform float pathSegment;
Expand All @@ -124,48 +151,48 @@ export function modifyShader(material, uniforms, numberOfCurves = 1) {
// shader override
.replace(
/void\s*main\s*\(\)\s*\{/,
`
void main() {
#include <beginnormal_vertex>
vec4 worldPos = modelMatrix * vec4(position, 1.);
bool bend = flow > 0;
float xWeight = bend ? 0. : 1.;
#ifdef USE_INSTANCING
float pathOffsetFromInstanceMatrix = instanceMatrix[3][2];
float spineLengthFromInstanceMatrix = instanceMatrix[3][0];
float spinePortion = bend ? (worldPos.x + spineOffset) / spineLengthFromInstanceMatrix : 0.;
float mt = (spinePortion * pathSegment + pathOffset + pathOffsetFromInstanceMatrix)*textureStacks;
#else
float spinePortion = bend ? (worldPos.x + spineOffset) / spineLength : 0.;
float mt = (spinePortion * pathSegment + pathOffset)*textureStacks;
#endif
mt = mod(mt, textureStacks);
float rowOffset = floor(mt);
#ifdef USE_INSTANCING
rowOffset += instanceMatrix[3][1] * ${TEXTURE_HEIGHT}.;
#endif
vec3 spinePos = texture2D(spineTexture, vec2(mt, (0. + rowOffset + 0.5) / textureLayers)).xyz;
vec3 a = texture2D(spineTexture, vec2(mt, (1. + rowOffset + 0.5) / textureLayers)).xyz;
vec3 b = texture2D(spineTexture, vec2(mt, (2. + rowOffset + 0.5) / textureLayers)).xyz;
vec3 c = texture2D(spineTexture, vec2(mt, (3. + rowOffset + 0.5) / textureLayers)).xyz;
mat3 basis = mat3(a, b, c);
vec3 transformed = basis
* vec3(worldPos.x * xWeight, worldPos.y * 1., worldPos.z * 1.)
+ spinePos;
vec3 transformedNormal = normalMatrix * (basis * objectNormal);
/* glsl */ `
void main() {
#include <beginnormal_vertex>
vec4 worldPos = modelMatrix * vec4(position, 1.);
bool bend = flow > 0;
float xWeight = bend ? 0. : 1.;
#ifdef USE_INSTANCING
float pathOffsetFromInstanceMatrix = instanceMatrix[3][2];
float spineLengthFromInstanceMatrix = instanceMatrix[3][0];
float spinePortion = bend ? (worldPos.x + spineOffset) / spineLengthFromInstanceMatrix : 0.;
float mt = (spinePortion * pathSegment + pathOffset + pathOffsetFromInstanceMatrix)*textureStacks;
#else
float spinePortion = bend ? (worldPos.x + spineOffset) / spineLength : 0.;
float mt = (spinePortion * pathSegment + pathOffset)*textureStacks;
#endif
mt = mod(mt, textureStacks);
float rowOffset = floor(mt);
#ifdef USE_INSTANCING
rowOffset += instanceMatrix[3][1] * ${TEXTURE_HEIGHT}.;
#endif
vec3 spinePos = texture2D(spineTexture, vec2(mt, (0. + rowOffset + 0.5) / textureLayers)).xyz;
vec3 a = texture2D(spineTexture, vec2(mt, (1. + rowOffset + 0.5) / textureLayers)).xyz;
vec3 b = texture2D(spineTexture, vec2(mt, (2. + rowOffset + 0.5) / textureLayers)).xyz;
vec3 c = texture2D(spineTexture, vec2(mt, (3. + rowOffset + 0.5) / textureLayers)).xyz;
mat3 basis = mat3(a, b, c);
vec3 transformed = basis
* vec3(worldPos.x * xWeight, worldPos.y * 1., worldPos.z * 1.)
+ spinePos;
vec3 transformedNormal = normalMatrix * (basis * objectNormal);
`,
)
.replace(
'#include <project_vertex>',
`vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );
/* glsl */ `vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );
gl_Position = projectionMatrix * mvPosition;`,
)

Expand All @@ -176,16 +203,24 @@ vec3 transformedNormal = normalMatrix * (basis * objectNormal);
/**
* A helper class for making meshes bend aroudn curves
*/
export class Flow {
export class Flow<TMesh extends Mesh = Mesh> {
curveArray: Curve<any>[]
curveLengthArray: number[]

object3D: TMesh
splineTexure: DataTexture
uniforms: CurveModifierUniforms

/**
* @param {Mesh} mesh The mesh to clone and modify to bend around the curve
* @param {number} numberOfCurves The amount of space that should preallocated for additional curves
*/
constructor(mesh, numberOfCurves = 1) {
const obj3D = mesh.clone()
constructor(mesh: TMesh, numberOfCurves = 1) {
const obj3D = mesh.clone() as TMesh
const splineTexure = initSplineTexture(numberOfCurves)
const uniforms = getUniforms(splineTexure)
obj3D.traverse(function (child) {

obj3D.traverse((child) => {
if (child instanceof Mesh || child instanceof InstancedMesh) {
child.material = child.material.clone()
modifyShader(child.material, uniforms, numberOfCurves)
Expand All @@ -200,7 +235,7 @@ export class Flow {
this.uniforms = uniforms
}

updateCurve(index, curve) {
updateCurve<TCurve extends Curve<any>>(index: number, curve: TCurve): void {
if (index >= this.curveArray.length) throw Error('Index out of range for Flow')
const curveLength = curve.getLength()
this.uniforms.spineLength.value = curveLength
Expand All @@ -209,7 +244,7 @@ export class Flow {
updateSplineTexture(this.splineTexure, curve, index)
}

moveAlongCurve(amount) {
moveAlongCurve(amount: number): void {
this.uniforms.pathOffset.value += amount
}
}
Expand All @@ -218,15 +253,21 @@ const matrix = new Matrix4()
/**
* A helper class for creating instanced versions of flow, where the instances are placed on the curve.
*/
export class InstancedFlow extends Flow {
export class InstancedFlow<
TGeometry extends BufferGeometry = BufferGeometry,
TMaterial extends Material = Material
> extends Flow<InstancedMesh<TGeometry, TMaterial>> {
offsets: number[]
whichCurve: number[]

/**
*
* @param {number} count The number of instanced elements
* @param {number} curveCount The number of curves to preallocate for
* @param {Geometry} geometry The geometry to use for the instanced mesh
* @param {Material} material The material to use for the instanced mesh
*/
constructor(count, curveCount, geometry, material) {
constructor(count: number, curveCount: number, geometry: TGeometry, material: TMaterial) {
const mesh = new InstancedMesh(geometry, material, count)
mesh.instanceMatrix.setUsage(DynamicDrawUsage)
super(mesh, curveCount)
Expand All @@ -241,7 +282,7 @@ export class InstancedFlow extends Flow {
*
* @param {number} index of the instanced element to update
*/
writeChanges(index) {
writeChanges(index: number): void {
matrix.makeTranslation(this.curveLengthArray[this.whichCurve[index]], this.whichCurve[index], this.offsets[index])
this.object3D.setMatrixAt(index, matrix)
this.object3D.instanceMatrix.needsUpdate = true
Expand All @@ -253,7 +294,7 @@ export class InstancedFlow extends Flow {
* @param {number} index Which element to update
* @param {number} offset Move by how much
*/
moveIndividualAlongCurve(index, offset) {
moveIndividualAlongCurve(index: number, offset: number): void {
this.offsets[index] += offset
this.writeChanges(index)
}
Expand All @@ -264,7 +305,7 @@ export class InstancedFlow extends Flow {
* @param {number} index the index of the instanced element to update
* @param {number} curveNo the index of the curve it should use
*/
setCurve(index, curveNo) {
setCurve(index: number, curveNo: number): void {
if (isNaN(curveNo)) throw Error('curve index being set is Not a Number (NaN)')
this.whichCurve[index] = curveNo
this.writeChanges(index)
Expand Down
4 changes: 4 additions & 0 deletions src/types/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TUniform<TValue = any> = {
type?: string
value: TValue
}

0 comments on commit 8882d72

Please sign in to comment.