feat(tresjs): added tresjs package with new Tresjs v5 integrated post-processing components

Fix #605

- https://github.com/Tresjs/post-processing/issues/212

Co-authored-by: Iro <155815508+Iro96@users.noreply.github.com>
This commit is contained in:
Neko Ayaka
2025-09-28 14:12:13 +08:00
co-authored by Iro
parent 2709d12152
commit 7ea96b7c00
17 changed files with 571 additions and 12 deletions
+2
View File
@@ -74,6 +74,7 @@ words:
- dompurify
- dotenvx
- DownloadLive2DSDK
- Downsampling
- dreamlog
- dtolnay
- dtype
@@ -157,6 +158,7 @@ words:
- mousedown
- mouseup
- msvc
- multisampling
- Myriam
- ndarray
- Neko
-1
View File
@@ -39,7 +39,6 @@
},
"devDependencies": {
"@antfu/eslint-config": "^5.4.1",
"@antfu/ni": "^26.0.1",
"@arethetypeswrong/core": "^0.18.2",
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@iconify/utils": "^3.0.2",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@proj-airi/stage-ui-three",
"type": "module",
"version": "0.0.1",
"private": true,
"description": "A collection of 3D scene components that used by Project AIRI",
"author": {
"name": "Moeru AI Project AIRI Team",
@@ -24,9 +24,9 @@
"@pixiv/three-vrm": "^3.4.2",
"@pixiv/three-vrm-animation": "^3.4.2",
"@pixiv/three-vrm-core": "^3.4.2",
"@proj-airi/tresjs": "workspace:*",
"@tresjs/cientos": "^5.0.0",
"@tresjs/core": "^5.0.2",
"@tresjs/post-processing": "^2.4.0",
"@vueuse/core": "^13.9.0",
"culori": "^4.0.2",
"pinia": "^3.0.3",
@@ -1,6 +1,5 @@
<script setup lang="ts">
/*
* ThreeScene.vue
* - Root vue component of stage-ui-three package
* - This scene component the root for all the sub components in the 3d scene
* - This package, stage-ui-three, is a stateful package
@@ -13,8 +12,8 @@ import type { DirectionalLight, SphericalHarmonics3, Texture, WebGLRenderTarget
import type { Vec3 } from '../stores/model-store'
import { EffectComposerPmndrs, HueSaturationPmndrs } from '@proj-airi/tresjs/post-processing'
import { TresCanvas } from '@tresjs/core'
import { EffectComposerPmndrs, HueSaturationPmndrs } from '@tresjs/post-processing'
import { useElementBounding } from '@vueuse/core'
import { formatHex } from 'culori'
import { storeToRefs } from 'pinia'
@@ -283,6 +282,7 @@ defineExpose({
:height="height"
:tone-mapping="ACESFilmicToneMapping"
:tone-mapping-exposure="1"
:clear-alpha="0"
@ready="onTresReady"
>
<OrbitControls
@@ -86,7 +86,7 @@ vrmDialog.onChange(handleAddVRMModel)
<DropdownMenuPortal>
<DropdownMenuContent
class="will-change-[opacity,transform] z-10000 max-w-45 rounded-lg p-0.5 shadow-md outline-none data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade"
bg="neutral-700/50 dark:neutral-950/50"
bg="neutral-100/50 dark:neutral-950/50"
transition="colors duration-200 ease-in-out"
backdrop-blur-sm
align="end"
+33
View File
@@ -0,0 +1,33 @@
{
"name": "@proj-airi/tresjs",
"type": "module",
"private": true,
"description": "Shared Tres (effects, composables)",
"author": {
"name": "Moeru AI Project AIRI Team",
"email": "airi@moeru.ai",
"url": "https://github.com/moeru-ai"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "packages/tres"
},
"exports": {
"./post-processing": "./src/post-processing/index.ts"
},
"scripts": {
"typecheck": "vue-tsc --noEmit"
},
"peerDependencies": {
"@tresjs/cientos": ">=5.0.0 <6.0.0",
"@tresjs/core": ">=5.0.0 <6.0.0",
"postprocessing": ">=6.0.0 <7.0.0",
"three": ">=0.180.0 <0.181.0",
"vue": ">=3.5.0 <4.0.0"
},
"devDependencies": {
"@types/three": "^0.180.0"
}
}
@@ -0,0 +1,163 @@
<!--
https://github.com/Tresjs/post-processing/blob/c89b5b4353d889d58da4dd6746e64b32a3f711ec/src/core/pmndrs/EffectComposerPmndrs.vue
-->
<script lang="ts">
import type { EffectComposer } from 'postprocessing'
import type { WebGLRenderer } from 'three'
import type { InjectionKey, ShallowRef } from 'vue'
import WEBGL from 'three/examples/jsm/capabilities/WebGL.js'
import { useTres, useTresContext } from '@tresjs/core'
import {
DepthDownsamplingPass,
EffectComposer as EffectComposerImpl,
NormalPass,
RenderPass,
} from 'postprocessing'
import { HalfFloatType } from 'three'
import { computed, onUnmounted, provide, shallowRef, watch } from 'vue'
export const effectComposerInjectionKey: InjectionKey<ShallowRef<EffectComposer | null>> = Symbol('effectComposerPmndrs')
export interface EffectComposerPmndrsProps {
enabled?: boolean
depthBuffer?: boolean
disableNormalPass?: boolean
stencilBuffer?: boolean
resolutionScale?: number
autoClear?: boolean
multisampling?: number
frameBufferType?: number
}
</script>
<script setup lang="ts">
const props = withDefaults(defineProps<EffectComposerPmndrsProps>(), {
enabled: true,
autoClear: true,
frameBufferType: HalfFloatType,
disableNormalPass: false,
depthBuffer: undefined,
multisampling: 0,
stencilBuffer: undefined,
})
const emit = defineEmits(['render'])
const { renderer } = useTresContext()
const { scene, camera, sizes } = useTres()
const effectComposer: ShallowRef<EffectComposerImpl | null> = shallowRef(null)
let downSamplingPass: DepthDownsamplingPass | null = null
let normalPass: NormalPass | null = null
provide(effectComposerInjectionKey, effectComposer)
defineExpose({ composer: effectComposer })
function setNormalPass() {
if (!effectComposer.value) {
return
}
normalPass = new NormalPass(scene.value, camera.value)
normalPass.enabled = false
effectComposer.value.addPass(normalPass)
if (props.resolutionScale !== undefined && WEBGL.isWebGL2Available()) {
downSamplingPass = new DepthDownsamplingPass({
normalBuffer: normalPass.texture,
resolutionScale: props.resolutionScale,
})
downSamplingPass.enabled = false
effectComposer.value.addPass(downSamplingPass)
}
}
const effectComposerParams = computed(() => {
const plainEffectComposer = new EffectComposerImpl()
const params = {
depthBuffer: props.depthBuffer !== undefined
? props.depthBuffer
: plainEffectComposer.inputBuffer.depthBuffer,
stencilBuffer:
props.stencilBuffer !== undefined
? props.stencilBuffer
: plainEffectComposer.inputBuffer.stencilBuffer,
multisampling: WEBGL.isWebGL2Available()
? props.multisampling !== undefined
? props.multisampling
: plainEffectComposer.multisampling
: 0,
frameBufferType: props.frameBufferType !== undefined
? props.frameBufferType
: HalfFloatType,
}
plainEffectComposer.dispose()
return params
})
function initEffectComposer() {
if (!renderer && !scene.value && !camera.value) {
return
}
effectComposer.value?.dispose()
effectComposer.value = new EffectComposerImpl(renderer.instance as WebGLRenderer, effectComposerParams.value)
effectComposer.value.addPass(new RenderPass(scene.value, camera.value))
if (!props.disableNormalPass) {
setNormalPass()
}
}
watch([scene, camera, () => props.disableNormalPass], () => {
if (!sizes.width.value || !sizes.height.value) {
return
}
initEffectComposer()
})
watch(() => [sizes.width.value, sizes.height.value], ([width, height]) => {
// effect composer should only live once the canvas has a size > 0
if (!width && !height) {
return
}
if (effectComposer.value) {
effectComposer.value.setSize(width, height)
}
else {
initEffectComposer()
}
}, {
immediate: true,
})
renderer.replaceRenderFunction((notifySuccess) => {
if (props.enabled && renderer.instance && effectComposer.value && sizes.width.value && sizes.height.value) {
const currentAutoClear = renderer.instance.autoClear
renderer.instance.autoClear = props.autoClear
if (props.stencilBuffer && !props.autoClear) {
renderer.instance.clearStencil()
}
effectComposer.value.render()
emit('render', effectComposer.value)
renderer.instance.autoClear = currentAutoClear
notifySuccess()
}
})
onUnmounted(() => {
effectComposer.value?.dispose()
})
</script>
<template>
<slot />
</template>
@@ -0,0 +1,47 @@
<script lang="ts" setup>
import type { BlendFunction } from 'postprocessing'
import { HueSaturationEffect } from 'postprocessing'
import { useEffectPmndrs } from '../../composables/use-effect-pmndrs'
import { makePropWatchers } from '../../utils'
export interface HueSaturationPmndrsProps {
/**
* The saturation adjustment. A value of 0.0 results in grayscale, and 1.0 leaves saturation unchanged.
* Range: [0.0, 1.0]
*/
saturation?: number
/**
* The hue adjustment in radians.
* Range: [-π, π] (or [0, 2π] for a full rotation)
*/
hue?: number
/**
* The blend function. Defines how the effect blends with the original scene.
*/
blendFunction?: BlendFunction
}
const props = defineProps<HueSaturationPmndrsProps>()
const { pass, effect } = useEffectPmndrs(() => new HueSaturationEffect(props), props)
defineExpose({ pass, effect })
makePropWatchers(
[
[() => props.blendFunction, 'blendMode.blendFunction'],
[() => props.hue, 'hue'],
[() => props.saturation, 'saturation'],
],
effect,
() => new HueSaturationEffect(),
)
</script>
<template>
<slot />
</template>
@@ -0,0 +1 @@
export { default as HueSaturationPmndrs } from './HueSaturationPmndrs.vue'
@@ -0,0 +1,2 @@
export { default as EffectComposerPmndrs } from './EffectComposerPmndrs.vue'
export { HueSaturationPmndrs } from './effects'
@@ -0,0 +1 @@
export { useEffectPmndrs } from './use-effect-pmndrs'
@@ -0,0 +1,102 @@
/**
* https://github.com/Tresjs/post-processing/blob/c89b5b4353d889d58da4dd6746e64b32a3f711ec/src/core/pmndrs/EffectComposerPmndrs.vue
*/
import type { Effect } from 'postprocessing'
import type { Reactive, ShallowRef } from 'vue'
import { useTres } from '@tresjs/core'
import { EffectPass } from 'postprocessing'
import { inject, nextTick, onUnmounted, shallowRef, watch, watchEffect } from 'vue'
import { effectComposerInjectionKey } from '../components/EffectComposerPmndrs.vue'
/**
* @param newEffectFunction - A function that returns a new effect instance.
* @param passDependencies - A reactive object that the pass depends on (usually props). Changes to this object will trigger re-rendering.
* @param dependencyFieldsTriggeringRecreation - fields in passDependencies that require effect recreation when changed
*/
export function useEffectPmndrs<T extends Effect, D extends Record<PropertyKey, any>>(newEffectFunction: () => T, passDependencies: Reactive<D>, dependencyFieldsTriggeringRecreation?: (keyof D)[]): {
pass: ShallowRef<EffectPass | null>
effect: ShallowRef<T | null>
} {
const composer = inject(effectComposerInjectionKey)
const pass = shallowRef<EffectPass | null>(null) as ShallowRef<EffectPass | null>
const effect = shallowRef<T | null>(null) as ShallowRef<T | null>
const { scene, camera, invalidate } = useTres()
watch(passDependencies, () => invalidate())
const removePass = () => {
if (pass.value) {
composer?.value?.removePass(pass.value)
}
effect.value?.dispose()
pass.value?.dispose()
}
const createEffect = (index?: number) => {
if (!camera.value || !composer?.value || !scene.value) {
return
}
effect.value = newEffectFunction()
pass.value = new EffectPass(camera.value, effect.value)
composer.value.addPass(pass.value, index)
}
// Watch for changes in props that require effect recreation
if (dependencyFieldsTriggeringRecreation) {
watch(
() => dependencyFieldsTriggeringRecreation.map(field => passDependencies[field]),
() => {
if (!composer?.value) {
return
}
const index = composer.value?.passes.findIndex(p => p === pass.value)
if (!~index) {
return
}
removePass()
createEffect(index)
},
)
}
watchEffect(() => {
if (!camera.value || !effect?.value) {
return
}
effect.value.mainCamera = camera.value
})
// Initial effect creation
const unwatch = watchEffect(() => {
if (!camera.value || !composer?.value || !scene.value) {
return
}
nextTick(() => unwatch())
if (effect.value) {
return
}
createEffect()
})
onUnmounted(() => {
removePass()
})
return {
pass,
effect,
}
}
@@ -0,0 +1,15 @@
export {
EffectComposerPmndrs,
HueSaturationPmndrs,
} from './components'
export {
useEffectPmndrs,
} from './composables'
export {
get,
makePropWatcher,
makePropWatchers,
makePropWatchersUsingAllProps,
omit,
set,
} from './utils'
@@ -0,0 +1,2 @@
export { get, omit, set } from './object'
export { makePropWatcher, makePropWatchers, makePropWatchersUsingAllProps } from './props'
@@ -0,0 +1,90 @@
const pathRegex = /([^[.\]])+/g
/**
* Retrieves the value at a given path within a provided object.
*
* @template T - The type of value to be returned
*
* @param {any} obj - The object to extract value from
* @param {string | string[]} path - A path or an array of path where the value should be get from
*
* @returns {T | undefined} - The value at the given path in the object, or undefined if path is not found
*
* @example
*
* const obj = { a: { b: { c: 1 } } }
*
* const result = get(obj, 'a.b.c')
*
* console.log(result) // 1
*/
export function get<T>(obj: any, path: string | string[]): T | undefined {
if (!path) {
return undefined
}
const pathArray = Array.isArray(path)
? path
: path.match(pathRegex)
return pathArray?.reduce((prevObj, key) => prevObj && prevObj[key], obj)
}
/**
* Sets a value at a given path within a provided object. If the path does not exist, nested objects will be created.
*
* @param {any} obj - The original object to set value in
* @param {string | string[]} path - A path or an array of path where the value should be set
* @param {any} value - The value to be set at the provided path
*
* @returns {void}
*
* @example
* const obj = { a: { b: { c: 1 } } }
*
* set(obj, 'a.b.c', 2)
*
* console.log(obj) // { a: { b: { c: 2 } } }
*/
export function set(obj: any, path: string | string[], value: any): void {
const pathArray = Array.isArray(path)
? path
: path.match(pathRegex)
if (pathArray) {
pathArray.reduce((acc, key, i) => {
if (acc[key] === undefined) {
acc[key] = {}
}
if (i === pathArray.length - 1) {
acc[key] = value
}
return acc[key]
}, obj)
}
}
/**
* Omits given properties from a provided object.
*
* @template T - An object with string keys and any type of values
*
* @param {T} obj - The original object to omit properties from
* @param {(keyof T)[]} properties - An array of property key names to omit from the base object
*
* @returns {Partial<T>} The new object with omitted properties
*
* @example
* const obj = { a: 1, b: 2, c: 3 }
* const propsToOmit = ['b', 'c']
*
* const newObj = omit(obj, propsToOmit)
*
* console.log(newObj) // { a: 1 }
*/
export function omit<T extends Record<string, any>>(obj: T, properties: (keyof T)[]): Partial<T> {
const newObj = { ...obj }
properties.forEach(prop => delete newObj[prop])
return newObj
}
@@ -0,0 +1,83 @@
import type { Ref, WatchOptions } from 'vue'
import { watch } from 'vue'
import { get, set } from './object'
/**
* Creates a prop watcher function that monitors changes to a property and updates a target object.
*
* @template T - The type of the property being watched.
* @template E - The type of the target object.
* @param {() => T} propGetter - A function that retrieves the prop value to be watched.
* @param {Ref<E>} target - A Ref representing the target object to be updated.
* @param {string} propertyPath - The dot-separated path to the property within the target object.
* @param {() => E & { dispose?(): void }} newPlainObjectFunction - A function that creates a new plain object to retrieve the defaults from with an optional "dispose" method for cleanup.
* @param {WatchOptions} watchOptions - The options for watch.
*/
export function makePropWatcher<T, E>(
propGetter: () => T,
target: Ref<E>,
propertyPath: string,
newPlainObjectFunction: () => E & { dispose?: () => void },
watchOptions: WatchOptions = {},
) {
return watch(propGetter, (newValue) => {
if (!target.value) {
return
}
if (newValue === undefined) {
const plainObject = newPlainObjectFunction()
set(target.value, propertyPath, get(plainObject, propertyPath))
plainObject.dispose?.()
}
else {
set(target.value, propertyPath, propGetter())
}
}, watchOptions)
}
/**
* Creates multiple prop watchers for monitoring changes to multiple properties and updating a target object.
*
* @template T - The type of the property being watched.
* @template E - The type of the target object.
* @param {(string | (() => T))[][]} propGettersAndPropertyPaths - An array of arrays containing pairs of prop getters and their corresponding property paths within the target object.
* @param {Ref<E>} target - A Ref representing the target object to be updated.
* @param {() => E & { dispose?(): void }} newPlainObjectFunction - A function that creates a new plain object to retrieve the defaults from with an optional "dispose" method for cleanup.
*/
export function makePropWatchers<E>(
propGettersAndPropertyPaths: (string | (() => any))[][],
target: Ref<E>,
newPlainObjectFunction: () => E & { dispose?: () => void },
) {
return propGettersAndPropertyPaths.map(([propGetterFn, path]) => makePropWatcher(
propGetterFn as () => any,
target,
path as string,
newPlainObjectFunction,
))
}
/**
* Creates multiple prop watchers via the props object for monitoring changes to multiple properties and updating a target object.
* Use this method in case the prop names match the names of the properties you want to set on your target object.
*
* @param props - The props object. Usually created via defineProps.
* @param {Ref<E>} target - A Ref representing the target object to be updated.
* @param {() => E & { dispose?(): void }} newPlainObjectFunction - A function that creates a new plain object to retrieve the defaults from with an optional "dispose" method for cleanup.
*/
export function makePropWatchersUsingAllProps<E>(
props: { [key: PropertyKey]: any },
target: Ref<E>,
newPlainObjectFunction: () => E & { dispose?: () => void },
) {
return Object.keys(props).map(key => makePropWatcher(
() => props[key],
target,
key,
newPlainObjectFunction,
))
}
+25 -6
View File
@@ -79,9 +79,6 @@ importers:
'@antfu/eslint-config':
specifier: ^5.4.1
version: 5.4.1(@unocss/eslint-plugin@66.5.2(eslint@9.36.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.22)(eslint-plugin-format@1.0.1(eslint@9.36.0(jiti@2.5.1)))(eslint@9.36.0(jiti@2.5.1))(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(jiti@2.5.1)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(less@4.4.1)(lightningcss@1.30.1)(msw@2.7.3(@types/node@24.5.2)(typescript@5.9.2))(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
'@antfu/ni':
specifier: ^26.0.1
version: 26.0.1
'@arethetypeswrong/core':
specifier: ^0.18.2
version: 0.18.2
@@ -1985,15 +1982,15 @@ importers:
'@pixiv/three-vrm-core':
specifier: ^3.4.2
version: 3.4.2(three@0.180.0)
'@proj-airi/tresjs':
specifier: workspace:*
version: link:../tresjs
'@tresjs/cientos':
specifier: ^5.0.0
version: 5.0.0(@tresjs/core@5.0.2(three@0.180.0)(vue@3.5.22(typescript@5.9.2)))(@types/three@0.180.0)(react@18.3.1)(three@0.180.0)(vue@3.5.22(typescript@5.9.2))
'@tresjs/core':
specifier: ^5.0.2
version: 5.0.2(three@0.180.0)(vue@3.5.22(typescript@5.9.2))
'@tresjs/post-processing':
specifier: ^2.4.0
version: 2.4.0(@tresjs/core@5.0.2(three@0.180.0)(vue@3.5.22(typescript@5.9.2)))(three@0.180.0)(typescript@5.9.2)(vue@3.5.22(typescript@5.9.2))
'@vueuse/core':
specifier: ^13.9.0
version: 13.9.0(vue@3.5.22(typescript@5.9.2))
@@ -2033,6 +2030,28 @@ importers:
specifier: ^2.15.4
version: 2.15.4(typescript@5.9.2)
packages/tresjs:
dependencies:
'@tresjs/cientos':
specifier: '>=5.0.0 <6.0.0'
version: 5.0.0(@tresjs/core@5.0.2(three@0.180.0)(vue@3.5.22(typescript@5.9.2)))(@types/three@0.180.0)(react@18.3.1)(three@0.180.0)(vue@3.5.22(typescript@5.9.2))
'@tresjs/core':
specifier: '>=5.0.0 <6.0.0'
version: 5.0.2(three@0.180.0)(vue@3.5.22(typescript@5.9.2))
postprocessing:
specifier: '>=6.0.0 <7.0.0'
version: 6.37.8(three@0.180.0)
three:
specifier: '>=0.180.0 <0.181.0'
version: 0.180.0
vue:
specifier: '>=3.5.0 <4.0.0'
version: 3.5.22(typescript@5.9.2)
devDependencies:
'@types/three':
specifier: ^0.180.0
version: 0.180.0
packages/ui:
dependencies:
'@vueuse/core':