diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index ffe24e8db..39094c288 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -78,6 +78,7 @@ export default defineConfig({ { text: 'Edges', link: '/guide/abstractions/edges' }, { text: 'PositionalAudio', link: '/guide/abstractions/positional-audio' }, { text: 'AnimatedSprite', link: '/guide/abstractions/animated-sprite' }, + { text: 'Image', link: '/guide/abstractions/image' }, { text: 'Billboard', link: '/guide/abstractions/billboard' }, ], }, diff --git a/docs/.vitepress/theme/components/ImageDemo.vue b/docs/.vitepress/theme/components/ImageDemo.vue new file mode 100644 index 000000000..414624d11 --- /dev/null +++ b/docs/.vitepress/theme/components/ImageDemo.vue @@ -0,0 +1,31 @@ + + + diff --git a/docs/component-list/components.ts b/docs/component-list/components.ts index 3c9621373..22c720ccc 100644 --- a/docs/component-list/components.ts +++ b/docs/component-list/components.ts @@ -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' }, { text: 'Billboard', link: '/guide/abstractions/billboard' }, ], }, diff --git a/docs/guide/abstractions/image.md b/docs/guide/abstractions/image.md new file mode 100644 index 000000000..2d522d77a --- /dev/null +++ b/docs/guide/abstractions/image.md @@ -0,0 +1,33 @@ +# 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 +`` 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. | | diff --git a/playground/vue/src/pages/abstractions/ImageDemo.vue b/playground/vue/src/pages/abstractions/ImageDemo.vue new file mode 100644 index 000000000..67706c588 --- /dev/null +++ b/playground/vue/src/pages/abstractions/ImageDemo.vue @@ -0,0 +1,89 @@ + + + diff --git a/playground/vue/src/router/routes/abstractions.ts b/playground/vue/src/router/routes/abstractions.ts index 04c686ac3..ad09164d2 100644 --- a/playground/vue/src/router/routes/abstractions.ts +++ b/playground/vue/src/router/routes/abstractions.ts @@ -64,6 +64,11 @@ export const abstractionsRoutes = [ name: 'AnimatedSprite', component: () => import('../../pages/abstractions/AnimatedSpriteDemo.vue'), }, + { + path: '/abstractions/image', + name: 'Image', + component: () => import('../../pages/abstractions/ImageDemo.vue'), + }, { path: '/abstractions/billboard', name: 'Billboard', diff --git a/src/core/abstractions/Image/ImageMaterial.vue b/src/core/abstractions/Image/ImageMaterial.vue new file mode 100644 index 000000000..782ffc4ef --- /dev/null +++ b/src/core/abstractions/Image/ImageMaterial.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/core/abstractions/Image/ImageMaterialImpl.ts b/src/core/abstractions/Image/ImageMaterialImpl.ts new file mode 100644 index 000000000..fcae35040 --- /dev/null +++ b/src/core/abstractions/Image/ImageMaterialImpl.ts @@ -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 + #include + } + `, +) + +export default imageMaterialImpl diff --git a/src/core/abstractions/Image/component.vue b/src/core/abstractions/Image/component.vue new file mode 100644 index 000000000..1c966d249 --- /dev/null +++ b/src/core/abstractions/Image/component.vue @@ -0,0 +1,122 @@ + + + diff --git a/src/core/abstractions/index.ts b/src/core/abstractions/index.ts index 553bbe06f..627987ebb 100644 --- a/src/core/abstractions/index.ts +++ b/src/core/abstractions/index.ts @@ -1,6 +1,7 @@ import AnimatedSprite from './AnimatedSprite/component.vue' import Billboard from './Billboard.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' @@ -21,6 +22,7 @@ export { Edges, Fbo, GlobalAudio, + Image, Lensflare, Levioso, MouseParallax, diff --git a/src/utils/shaderMaterial.ts b/src/utils/shaderMaterial.ts new file mode 100644 index 000000000..6e6ff864a --- /dev/null +++ b/src/utils/shaderMaterial.ts @@ -0,0 +1,82 @@ +/* +MIT License + +Copyright (c) 2020 react-spring + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import * as THREE from 'three' + +export function shaderMaterial( + uniforms: { + [name: string]: + | THREE.CubeTexture + | THREE.Texture + | Int32Array + | Float32Array + | THREE.Matrix4 + | THREE.Matrix3 + | THREE.Quaternion + | THREE.Vector4 + | THREE.Vector3 + | THREE.Vector2 + | THREE.Color + | number + | boolean + | Array + | null + }, + vertexShader: string, + fragmentShader: string, + onInit?: (material?: THREE.ShaderMaterial) => void, +) { + const material = class extends THREE.ShaderMaterial { + public key: string = '' + constructor(parameters = {}) { + const entries = Object.entries(uniforms) + // Create unforms and shaders + super({ + uniforms: entries.reduce((acc, [name, value]) => { + const uniform = THREE.UniformsUtils.clone({ [name]: { value } }) + return { + ...acc, + ...uniform, + } + }, {}), + vertexShader, + fragmentShader, + }) + // Create getter/setters + entries.forEach(([name]) => + Object.defineProperty(this, name, { + get: () => this.uniforms[name].value, + set: v => (this.uniforms[name].value = v), + }), + ) + + // Assign parameters, this might include uniforms + Object.assign(this, parameters) + // Call onInit + if (onInit) { onInit(this) } + } + } as unknown as typeof THREE.ShaderMaterial & { key: string } + material.key = THREE.MathUtils.generateUUID() + return material +}