Skip to content

Commit

Permalink
feat(Image): add component, playground, docs
Browse files Browse the repository at this point in the history
  • Loading branch information
andretchen0 committed Nov 22, 2024
1 parent fc6f2bc commit 3e4cb5d
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export default defineConfig({
{ text: 'useSurfaceSampler', link: '/guide/abstractions/use-surface-sampler' },
{ text: 'Sampler', link: '/guide/abstractions/sampler' },
{ text: 'AnimatedSprite', link: '/guide/abstractions/animated-sprite' },
{ text: 'Image', link: '/guide/abstractions/image' },
],
},
{
Expand Down
31 changes: 31 additions & 0 deletions docs/.vitepress/theme/components/ImageDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { NoToneMapping, SRGBColorSpace } from 'three'
import { TresCanvas } from '@tresjs/core'
import { Image, OrbitControls } from '@tresjs/cientos'
const gl = {
clearColor: '#82DBC5',
outputColorSpace: SRGBColorSpace,
toneMapping: NoToneMapping,
}
const URL_STUB = 'https://upload.wikimedia.org/wikipedia/commons/'
const URLS = [
'f/f0/Cyanistes_caeruleus_Oulu_20150516.JPG',
'3/36/Cyanistes_caeruleus_Oulu_20130323.JPG',
'2/2e/Cyanistes_caeruleus_Oulu_20170507_02.jpg',
].map(url => URL_STUB + url)
</script>

<template>
<TresCanvas
v-bind="gl"
>
<TresPerspectiveCamera :position="[0, 0, 2]" />
<OrbitControls />
<Image :url="URLS[0]" :radius="0.2" :transparent="true" :position="[-1.5, 0, -1]" />
<Image :url="URLS[1]" :radius="0.2" :transparent="true" />
<Image :url="URLS[2]" :radius="0.2" :transparent="true" :position="[1.5, 0, -1]" />
</TresCanvas>
</template>
1 change: 1 addition & 0 deletions docs/component-list/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default [
},
{ text: 'Sampler', link: '/guide/abstractions/sampler' },
{ text: 'PositionalAudio', link: '/guide/abstractions/positional-audio' },
{ text: 'Image', link: '/guide/abstractions/image' },
],
},
{
Expand Down
33 changes: 33 additions & 0 deletions docs/guide/abstractions/image.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Image

<DocsDemo>
<ImageDemo />
</DocsDemo>

`<Image />` is a shader-based component that optionally loads then displays an image texture on a default plane or on your custom geometry.

## Usage

<<< @/.vitepress/theme/components/ImageDemo.vue

## Props

::: info
`<Image />` is a THREE.Mesh and most Mesh attributes can be used as props on the component.
:::

| Prop | Description | Default |
| :--------------- | :--------------------------------------------------- | ------------- |
| `segments` | Number of divisions in the default geometry. | `1` |
| `scale` | Scale of the geometry. `number \| [number, number]` | `1` |
| `color` | Color multiplied into the image texture. | `'white'` |
| `zoom` | Shrinks or enlarges the image texture. | `1` |
| `radius` | Border radius applied to the image texture. (Intended for rectangular geometries. Use with `transparent`.) | `0` |
| `grayscale` | Power of grayscale effect. 0 is off. 1 is full grayscale. | `0` |
| `toneMapped` | Whether this material is tone mapped according to the renderers toneMapping settings. [See THREE.material.tonemapped](https://threejs.org/docs/?q=material#api/en/materials/Material.toneMapped) | `0` |
| `transparent` | Whether the image material should be transparent. [See THREE.material.transparent](https://threejs.org/docs/?q=material#api/en/materials/Material.transparent) | `false` |
| `transparent` | Whether the image material should be transparent. [See THREE.material.transparent](https://threejs.org/docs/?q=material#api/en/materials/Material.transparent) | `false` |
| `opacity` | Opacity of the image material. [See THREE.material.transparent](https://threejs.org/docs/?q=material#api/en/materials/Material.transparent) | `1` |
| `side` | THREE.Side of the image material. [See THREE.material.side](https://threejs.org/docs/?q=material#api/en/materials/Material.side) | `FrontSide` |
| `texture` | Image texture to display on the geometry. | |
| `url` | Image URL to load and display on the geometry. | |
58 changes: 58 additions & 0 deletions playground/vue/src/pages/abstractions/ImageDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import { Image, OrbitControls } from '@tresjs/cientos'
import { Color, NoToneMapping } from 'three'
const URLS = [
'https://upload.wikimedia.org/wikipedia/commons/1/13/20220713-great-tit.jpg',
'https://upload.wikimedia.org/wikipedia/commons/0/00/Friendly_Robin.jpg',
]
const opacity = shallowRef(1)
const url = shallowRef(URLS[0])
let elapsed = 0
setInterval(() => {
elapsed += 0.01
opacity.value = Math.abs(Math.sin(elapsed))
url.value = Math.sin(elapsed) > 0 ? URLS[0] : URLS[1]
}, 1000 / 30)
</script>

<template>
<TresCanvas :tone-mapping="NoToneMapping">
<TresPerspectiveCamera />
<OrbitControls />
<Image
:url="url"
:radius="opacity"
:color="new Color('white')"
>
<TresBoxGeometry />
</Image>
<Image
:position-y="2"
:scale="0.5"
:tone-mapped="true"
:url="url"
/>
<Image
:position-x="2"
:scale="[2, 1]"
:url="url"
/>
<Image
:position-y="-2"
:url="url"
:radius="opacity"
>
<TresCircleGeometry />
</Image>

<Image
:position-x="-2"
:url="url"
:radius="opacity"
:transparent="true"
/>
</TresCanvas>
</template>
5 changes: 5 additions & 0 deletions playground/vue/src/router/routes/abstractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,9 @@ export const abstractionsRoutes = [
name: 'AnimatedSprite',
component: () => import('../../pages/abstractions/AnimatedSpriteDemo.vue'),
},
{
path: '/abstractions/image',
name: 'Image',
component: () => import('../../pages/abstractions/ImageDemo.vue'),
},
]
15 changes: 15 additions & 0 deletions src/core/abstractions/Image/ImageMaterial.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { shallowRef } from 'vue'
import { extend } from '@tresjs/core'
import ImageMaterial from './ImageMaterialImpl'
extend({ ImageMaterial })
const materialRef = shallowRef()
defineExpose({ instance: materialRef })
</script>

<template>
<TresImageMaterial ref="materialRef" />
</template>
79 changes: 79 additions & 0 deletions src/core/abstractions/Image/ImageMaterialImpl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { shaderMaterial } from './../../../utils/shaderMaterial'
import { Color, Vector2 } from 'three'

/**
* NOTE: Source:
* https://threejs.org/docs/?q=material#api/en/materials/Material.transparent
*/
const imageMaterialImpl = shaderMaterial(
{
color: /* @__PURE__ */ new Color('white'),
scale: /* @__PURE__ */ new Vector2(1, 1),
imageBounds: /* @__PURE__ */ new Vector2(1, 1),
resolution: 1024,
map: null,
zoom: 1,
radius: 0,
grayscale: 0,
opacity: 1,
},
/* glsl */ `
varying vec2 vUv;
varying vec2 vPos;
void main() {
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.);
vUv = uv;
vPos = position.xy;
}
`,
/* glsl */ `
// mostly from https://gist.github.com/statico/df64c5d167362ecf7b34fca0b1459a44
varying vec2 vUv;
varying vec2 vPos;
uniform vec2 scale;
uniform vec2 imageBounds;
uniform float resolution;
uniform vec3 color;
uniform sampler2D map;
uniform float radius;
uniform float zoom;
uniform float grayscale;
uniform float opacity;
const vec3 luma = vec3(.299, 0.587, 0.114);
vec4 toGrayscale(vec4 color, float intensity) {
return vec4(mix(color.rgb, vec3(dot(color.rgb, luma)), intensity), color.a);
}
vec2 aspect(vec2 size) {
return size / min(size.x, size.y);
}
const float PI = 3.14159265;
// from https://iquilezles.org/articles/distfunctions
float udRoundBox( vec2 p, vec2 b, float r ) {
return length(max(abs(p)-b+r,0.0))-r;
}
void main() {
vec2 s = aspect(scale);
vec2 i = aspect(imageBounds);
float rs = s.x / s.y;
float ri = i.x / i.y;
vec2 new = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x);
vec2 offset = (rs < ri ? vec2((new.x - s.x) / 2.0, 0.0) : vec2(0.0, (new.y - s.y) / 2.0)) / new;
vec2 uv = vUv * s / new + offset;
vec2 zUv = (uv - vec2(0.5, 0.5)) / zoom + vec2(0.5, 0.5);
vec2 res = vec2(scale * resolution);
vec2 halfRes = 0.5 * res;
float b = udRoundBox(vUv.xy * res - halfRes, halfRes, resolution * radius);
vec3 a = mix(vec3(1.0,0.0,0.0), vec3(0.0,0.0,0.0), smoothstep(0.0, 1.0, b));
gl_FragColor = toGrayscale(texture2D(map, zUv) * vec4(color, opacity * a), grayscale);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}
`,
)

export default imageMaterialImpl
116 changes: 116 additions & 0 deletions src/core/abstractions/Image/component.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<script setup lang="ts">
import type { Side, Texture } from 'three'
import { Color, FrontSide } from 'three'
import type { TresColor } from '@tresjs/core'
import { useTexture, useTres } from '@tresjs/core'
import { computed, shallowRef, watch } from 'vue'
import ImageMaterial from './ImageMaterial.vue'
export type ImageProps = {
/**
* Number of divisions in the the default geometry.
*/
segments?: number
/**
* Scale of the geometry.
*/
scale?: number | [number, number]
/**
* Color multiplied into the image texture. Default is white.
*/
color?: TresColor
/**
* Shrinks or enlarges the image texture.
*/
zoom?: number
/**
* Border radius applied to image texture. Intended for rectangular geometries.
*/
radius?: number
/**
* Power of grayscale effect. 0 is no grayscale. 1 is full grayscale.
*/
grayscale?: number
/**
* Whether this material is tone mapped according to the renderer's toneMapping setting. [See THREE.material.tonemapped](https://threejs.org/docs/?q=material#api/en/materials/Material.toneMapped)
*/
toneMapped?: boolean
/**
* Whether the image material should be transparent. [See THREE.material.transparent](https://threejs.org/docs/?q=material#api/en/materials/Material.transparent)
*/
transparent?: boolean
/**
* Opacity of the image material. [See THREE.material.transparent](https://threejs.org/docs/?q=material#api/en/materials/Material.transparent)
*/
opacity?: number
/**
* THREE.Side of the image material. [See THREE.material.side](https://threejs.org/docs/?q=material#api/en/materials/Material.side)
*/
side?: Side
} & ({
/**
* Image texture to display on the geometry.
*/
texture: Texture
url?: never
} | {
texture?: never
/**
* Image URL to load and display on the geometry.
*/
url: string
})
const props = withDefaults(defineProps<ImageProps>(), {
segments: 1,
scale: 1,
color: () => new Color('white'),
zoom: 1,
radius: 0,
grayscale: 0,
toneMapped: true,
transparent: false,
opacity: 1,
side: FrontSide,
})
const imageRef = shallowRef()
const texture = shallowRef<Texture | null>(props.texture ?? null)
const size = useTres().sizes
const planeBounds = computed(() => Array.isArray(props.scale) ? [props.scale[0], props.scale[1]] : [props.scale, props.scale])
const imageBounds = computed(() => [texture.value?.image.width ?? 0, texture.value?.image.height ?? 0])
const resolution = computed(() => Math.max(size.width.value, size.height.value))
watch(() => [props.texture, props.url], () => {
if (props.texture) {
texture.value = props.texture
}
else {
useTexture([props.url!]).then(t => texture.value = t)
}
}, { immediate: true })
defineExpose({ instance: imageRef })
</script>

<template>
<TresMesh ref="imageRef" :scale="Array.isArray(props.scale) ? [...props.scale, 1] : props.scale">
<slot>
<TresPlaneGeometry :args="[1, 1, props.segments, props.segments]" />
</slot>
<ImageMaterial
:color="props.color"
:map="texture"
:zoom="props.zoom"
:grayscale="props.grayscale"
:opacity="props.opacity"
:scale="planeBounds"
:imageBounds="imageBounds"
:resolution="resolution"
:radius="radius"
:toneMapped="toneMapped"
:transparent="transparent"
:side="side"
/>
</TresMesh>
</template>
2 changes: 2 additions & 0 deletions src/core/abstractions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AnimatedSprite from './AnimatedSprite/component.vue'
import { GlobalAudio } from './GlobalAudio'
import Image from './Image/component.vue'
import Lensflare from './Lensflare/component.vue'
import Levioso from './Levioso.vue'
import MouseParallax from './MouseParallax.vue'
Expand All @@ -17,6 +18,7 @@ export {
AnimatedSprite,
Fbo,
GlobalAudio,
Image,
Lensflare,
Levioso,
MouseParallax,
Expand Down
6 changes: 3 additions & 3 deletions src/core/staging/ContactShadows.vue
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,9 @@ function setColors(ps: typeof props, pool: ReturnType<typeof init>) {
const { r, g, b } = tint
shader.fragmentShader = /* glsl */`
${shader.fragmentShader.replace(
'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );',
`gl_FragColor = vec4( ${r}, ${g}, ${b}, ( 1.0 - fragCoordZ ) * opacity);`,
)}
'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );',

Check failure on line 315 in src/core/staging/ContactShadows.vue

View workflow job for this annotation

GitHub Actions / Lint (20)

Expected indentation of 8 spaces but found 6
`gl_FragColor = vec4( ${r}, ${g}, ${b}, ( 1.0 - fragCoordZ ) * opacity);`,

Check failure on line 316 in src/core/staging/ContactShadows.vue

View workflow job for this annotation

GitHub Actions / Lint (20)

Expected indentation of 8 spaces but found 6
)}

Check failure on line 317 in src/core/staging/ContactShadows.vue

View workflow job for this annotation

GitHub Actions / Lint (20)

Expected indentation of 6 spaces but found 4
`
}
}
Expand Down
Loading

0 comments on commit 3e4cb5d

Please sign in to comment.