Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Superformula): add superformula #274

Merged
merged 5 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export default defineConfig({
{ text: 'Plane', link: '/guide/shapes/plane' },
{ text: 'Ring', link: '/guide/shapes/ring' },
{ text: 'Sphere', link: '/guide/shapes/sphere' },
{ text: 'Superformula', link: '/guide/shapes/superformula' },
{ text: 'Tetrahedron', link: '/guide/shapes/tetrahedron' },
{ text: 'Torus', link: '/guide/shapes/torus' },
{ text: 'TorusKnot', link: '/guide/shapes/torus-knot' },
Expand Down
16 changes: 16 additions & 0 deletions docs/.vitepress/theme/components/SuperformulaDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import { Superformula, OrbitControls } from '@tresjs/cientos'
</script>

<template>
<TresCanvas clear-color="#777">
<Superformula
:num-arms-b="24"
:exp-b="[40, 30, 20]"
>
<TresMeshNormalMaterial />
</Superformula>
<OrbitControls />
</TresCanvas>
</template>
38 changes: 38 additions & 0 deletions docs/.vitepress/theme/components/SuperformulaLechesDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { AmbientLight, Color, DirectionalLight, MeshPhongMaterial } from 'three'
import { TresCanvas } from '@tresjs/core'
import { Superformula } from '@tresjs/cientos'
import { useControls, TresLeches } from '@tresjs/leches'
import '@tresjs/leches/styles'

const { numArmsA, numArmsB, expA1 } = useControls({
numArmsA: { value: 1, min: 1, max: 40, step: 1 },
numArmsB: { value: 1, min: 1, max: 40, step: 0.1 },
expA1: { value: 8, min: 4, max: 40, step: 0.01 },
})

const material = new MeshPhongMaterial({ color: '#fbb03b', shininess: 1000 })
const directionalLight = new DirectionalLight('white', 4)
directionalLight.position.set(1, 1, 1)
const ambientLight = new AmbientLight('pink', 1)
</script>

<template>
<TresLeches class="important-top-4 important-left-4" />
<TresCanvas clear-color="#777">
<primitive :object="directionalLight" />
<primitive :object="ambientLight" />
<Superformula
:position="[1.5, 0.7, 0]"
:width-segments="128"
:height-segments="128"
:num-arms-a="numArmsA.value"
:num-arms-b="numArmsB.value"
:exp-a="[expA1.value, 8, 0]"
:exp-b="[2, 1, 2]"
color="orange"
>
<primitive :object="material" />
</Superformula>
</TresCanvas>
</template>
30 changes: 30 additions & 0 deletions docs/guide/shapes/superformula.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Superformula

<DocsDemo>
<SuperformulaLechesDemo />
</DocsDemo>

The `cientos` package provides a `<Superformula />` component that produces a configurable [3D plot of the superformula](https://en.wikipedia.org/wiki/Superformula).

## Usage
<DocsDemo>
<SuperformulaDemo />
</DocsDemo>

<<< @/.vitepress/theme/components/SuperformulaDemo.vue{3,8-13}

## Props

The `<Superformula />` 3D plot is the product of 2 2D superformulas, referred to as "A" and "B" in the props. See this [Wikipedia article about the superformula](https://en.wikipedia.org/wiki/Superformula) for more information about the function's arguments.

<table><thead><tr class="row-header"><th class="col-name">Name</th><th class="col-description">Description</th><th class="col-default">Default</th></tr></thead><tbody><tr class="row-width-segments"><td class="col-name"><strong><nobr>widthSegments</nobr></strong></td><td class="col-description">Number of horizontal mesh segments<br>
</td><td class="col-default"><code>32</code></td></tr><tr class="row-height-segments"><td class="col-name"><strong><nobr>heightSegments</nobr></strong></td><td class="col-description">Number of vertical mesh segments<br>
</td><td class="col-default"><code>32</code></td></tr><tr class="row-num-arms-a"><td class="col-name"><strong><nobr>numArmsA</nobr></strong></td><td class="col-description">For A, number of radial arms/ripples</td><td class="col-default"><code>4</code></td></tr><tr class="row-exp-a"><td class="col-name"><strong><nobr>expA</nobr></strong></td><td class="col-description">A's 3 exponents<br>
</td><td class="col-default"><code>[40,&nbsp;1.3,&nbsp;0.9]</code></td></tr><tr class="row-num-arms-b"><td class="col-name"><strong><nobr>numArmsB</nobr></strong></td><td class="col-description">For B, number of radial arms/ripples<br>
</td><td class="col-default"><code>4</code></td></tr><tr class="row-exp-b1"><td class="col-name"><strong><nobr>expB</nobr></strong></td><td class="col-description">B's 3 exponents<br>
</td><td class="col-default"><code>[40,&nbsp;1.3,&nbsp;0.9]</code></td></tr><tr class="row-color"><td class="col-name"><strong><nobr>color</nobr></strong></td><td class="col-description">If no material is provided, a color for the default material<br>
</td><td class="col-default"><code>'white'</code></td></tr></tbody></table>

## Slot

`<Superformula />` has a single slot for an optional material.
54 changes: 54 additions & 0 deletions playground/src/pages/shapes/SuperformulaDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { Color } from 'three'
import { TresCanvas, useRenderLoop } from '@tresjs/core'
import { OrbitControls, Superformula } from '@tresjs/cientos'

const numArmsA = shallowRef(1)
const numArmsB = shallowRef(1)
const expA1 = shallowRef(1)
const expA2 = shallowRef(1)
const expA3 = shallowRef(1)
const expB1 = shallowRef(1)
const expB2 = shallowRef(1)
const expB3 = shallowRef(1)

const { sin, cos } = Math

useRenderLoop().onLoop(({ elapsed }) => {
const e = elapsed * 0.1
numArmsA.value = sin(e * Math.PI) * 24
expA1.value = (sin(e * Math.PI) + 2 ) * 30
expA2.value = (sin(e * Math.E) + 2) * 30
expA3.value = (sin(e * Math.SQRT2) + 2) * 30
numArmsB.value = cos(e) * 24
expB1.value = (cos(e * Math.PI) + 2) * 30
expB2.value = (cos(e * Math.E) + 2) * 30
expB3.value = (cos(e * Math.SQRT2) + 2) * 30
})
</script>

<template>
<TresCanvas clear-color="#777">
<TresDirectionalLight
:position="[3, 2, 1]"
:intensity="8"
/>
<TresAmbientLight
:position="[3, 2, 1]"
:intensity="1"
:color="new Color('pink')"
/>
<Superformula
:width-segments="256"
:height-segments="256"
:num-arms-a="numArmsA"
:exp-a="[expA1, expA2, expA3]"
:num-arms-b="numArmsB"
:exp-b="[expB1, expB2, expB3]"
>
<TresMeshNormalMaterial />
</Superformula>
<TresGridHelper />
<OrbitControls />
</TresCanvas>
</template>
5 changes: 5 additions & 0 deletions playground/src/router/routes/shapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ export const shapesRoutes = [
name: 'Line2',
component: () => import('../../pages/shapes/Line2Demo.vue'),
},
{
path: '/shapes/superformula',
name: 'Superformula',
component: () => import('../../pages/shapes/SuperformulaDemo.vue'),
},
]
167 changes: 167 additions & 0 deletions src/core/shapes/Superformula.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<script setup lang="ts">
import { BufferAttribute, BufferGeometry } from 'three'
import { shallowRef, watch, onUnmounted } from 'vue'
import type { TresColor } from '@tresjs/core'

export type Float3 = [number, number, number]

export interface SuperFormulaProps {
/**
* Number of horizontal mesh segments
*/
widthSegments?: number
/**
* Number of vertical mesh segments
*/
heightSegments?: number
/**
* The 3D Superformula is the spherical product of 2 2D superformula curves: here called curves "A" and "B".
* Number of radial arms/ripples of A, corresponding to "m" [in this article.](https://en.wikipedia.org/wiki/Superformula)
*/
numArmsA?: number
/**
* A's 3 exponents
*/
expA?: Float3
/**
* For B, number of radial arms/ripples
*/
numArmsB?: number
/**
* B's 3 exponents
*/
expB?: Float3
/**
* If no material is provided, a color for the default material
*/
color?: TresColor
}

const props = withDefaults(defineProps<SuperFormulaProps>(), {
widthSegments: 32,
heightSegments: 32,
numArmsA: 4,
expA: () => [40, 1.3, 0.9],
numArmsB: 4,
expB: () => [40, 1.3, 0.9],
color: 'white',
})

const { cos, sin, abs } = Math

const geometry = shallowRef()
const color = shallowRef(props.color)

function makeGeometry(widthSegments: number, heightSegments: number) {
const geometry = new BufferGeometry()
const numPoints = widthSegments * heightSegments
const vertices = new Float32Array(new Array(3 * numPoints).fill(0))
const normals = new Float32Array(new Array(3 * numPoints).fill(0))
const indices: number[] = []
for (let h = 0; h < heightSegments - 1; h++) {
for (let w = 0; w < widthSegments - 1; w++) {
const tl = h * widthSegments + w
const tr = tl + 1
const bl = tl + widthSegments
const br = tr + widthSegments
indices.push(tl, bl, tr)
indices.push(bl, br, tr)
}
const tl = h * widthSegments + widthSegments - 1
const tr = h * widthSegments
const bl = tl + widthSegments
const br = tr + widthSegments
indices.push(tl, bl, tr)
indices.push(bl, br, tr)
}
geometry.setIndex(indices)
geometry.setAttribute('position', new BufferAttribute(vertices, 3))
geometry.setAttribute('normal', new BufferAttribute(normals, 3))
return geometry
}

// Source:
// https://en.wikipedia.org/wiki/Superformula
// NOTE: Superformula 2D
function r(theta: number, numArms: number, exp1: number, exp2: number, exp3: number): number {
return (abs(cos(numArms * theta * 0.25)) ** exp2 + abs(sin(numArms * theta * 0.25)) ** exp3) ** (-1 / exp1)
}

// NOTE: Superformula 3D
function updateGeometry(geometry: BufferGeometry,
numArmsA: number, expA1: number, expA2: number, expA3: number,
numArmsB: number, expB1: number, expB2: number, expB3: number,
widthSegments: number, heightSegments: number,
) {
const thetaStep = 2 * Math.PI / widthSegments
const thetaStart = -Math.PI
const phiStep = Math.PI / (heightSegments - 1)
const phiStart = -0.5 * Math.PI
const positionAttribute = geometry.getAttribute('position')

let i = 0
let theta = 0
let phi = phiStart
for (let pi = 0; pi < heightSegments; pi++) {
theta = thetaStart
for (let ti = 0; ti < widthSegments; ti++) {
const rA = r(theta, numArmsA, expA1, expA2, expA3)
const rB = r(phi, numArmsB, expB1, expB2, expB3)
positionAttribute.setXYZ(
i,
rA * cos(theta) * rB * cos(phi),
rB * sin(phi),
rA * sin(theta) * rB * cos(phi),
)
i++
theta += thetaStep
}
phi += phiStep
}
positionAttribute.needsUpdate = true
geometry.computeVertexNormals()
}

watch(() => props.color, () => color.value = props.color)

watch(() => [props.widthSegments, props.heightSegments], () => {
if (geometry.value) {
geometry.value.dispose()
}
geometry.value = makeGeometry(props.widthSegments, props.heightSegments)
}, { immediate: true })

watch(() => [
props.numArmsA, props.expA[0], props.expA[1], props.expA[2],
props.numArmsB, props.expB[0], props.expB[1], props.expB[2],
],
() => updateGeometry(geometry.value,
props.numArmsA, props.expA[0], props.expA[1], props.expA[2],
props.numArmsB, props.expB[0], props.expB[1], props.expB[2],
props.widthSegments, props.heightSegments,
), { immediate: true })

onUnmounted(() => {
if (geometry.value) {
geometry.value.dispose()
}
})

const superformulaRef = shallowRef()

defineExpose({
value: superformulaRef,
})
</script>

<template>
<TresMesh
ref="superformulaRef"
v-bind="$attrs"
:geometry="geometry"
>
<slot>
<TresMeshBasicMaterial :color="color" />
</slot>
</TresMesh>
</template>
2 changes: 2 additions & 0 deletions src/core/shapes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Octahedron from './Octahedron.vue'
import Plane from './Plane.vue'
import Ring from './Ring.vue'
import Sphere from './Sphere.vue'
import Superformula from './Superformula.vue'
import Tetrahedron from './Tetrahedron.vue'
import Torus from './Torus.vue'
import TorusKnot from './TorusKnot.vue'
Expand All @@ -26,6 +27,7 @@ export {
Plane,
Ring,
Sphere,
Superformula,
Tetrahedron,
Torus,
TorusKnot,
Expand Down