feat(all): 后处理迁移

This commit is contained in:
plum 2026-04-08 16:51:47 +08:00
parent 986da8ce42
commit 4cc239d660
25 changed files with 1644 additions and 66 deletions

View File

@ -226,6 +226,27 @@ export default {
"Post processing": "后期处理",
postProcessing: {
"Anti-aliasing": "抗锯齿",
"Quality Preset": "质量预设",
"Tone Mapping": "色调映射",
"Exposure": "曝光度",
"SSAO": "环境光遮蔽",
"Samples": "采样数",
"Rings": "环数",
"Bias": "偏置",
"Fade": "衰减",
"Luminance Influence": "亮度影响",
"Min Radius Scale": "最小半径缩放",
"Depth-aware Upsampling": "深度感知上采样",
"Distance Threshold": "距离阈值",
"Distance Falloff": "距离衰减",
"Range Threshold": "范围阈值",
"Range Falloff": "范围衰减",
"World Distance Threshold": "世界距离阈值",
"World Distance Falloff": "世界距离衰减",
"World Proximity Threshold": "世界邻近阈值",
"World Proximity Falloff": "世界邻近衰减",
"Colorize": "颜色化",
"AO Color": "遮蔽颜色",
"Outline": "描边线",
"Edge Strength": "边缘强度",
"Edge Glow": "边缘发光",
@ -237,6 +258,57 @@ export default {
"Radius": "半径",
"Threshold": "阈值",
"Strength": "强度",
"Brightness Contrast": "亮度对比度",
"Brightness": "亮度",
"Contrast": "对比度",
"Chromatic Aberration": "色差",
"Offset X": "偏移 X",
"Offset Y": "偏移 Y",
"Radial Modulation": "径向调制",
"Modulation Offset": "调制偏移",
"Color Depth": "色深",
"Bits": "位数",
"Hue Saturation": "色相饱和度",
"Hue": "色相",
"Saturation": "饱和度",
"Tilt Shift": "移轴模糊",
"Offset": "偏移",
"Rotation": "旋转",
"Focus Area": "焦点区域",
"Feather": "羽化",
"Scanline": "扫描线",
"Density": "密度",
"Scroll Speed": "滚动速度",
"Glitch": "故障",
"Glitch Mode": "故障模式",
"Sporadic": "偶发",
"Constant Mild": "持续轻微",
"Constant Wild": "持续剧烈",
"Delay Min": "延迟最小值",
"Delay Max": "延迟最大值",
"Duration Min": "持续最小值",
"Duration Max": "持续最大值",
"Strength Min": "强度最小值",
"Strength Max": "强度最大值",
"Ratio": "出现概率",
"Lens Distortion": "镜头畸变",
"Distortion X": "畸变 X",
"Distortion Y": "畸变 Y",
"Principal Point X": "主点 X",
"Principal Point Y": "主点 Y",
"Focal Length X": "焦距 X",
"Focal Length Y": "焦距 Y",
"Skew": "倾斜",
"Shock Wave": "冲击波",
"Click on scene to trigger shock wave effect": "点击场景触发冲击波效果",
"Amplitude": "振幅",
"Wave Size": "波浪大小",
"Speed": "传播速度",
"Max Radius": "最大半径",
"Click Trigger": "点击触发",
"Vignette": "暗角",
"Darkness": "暗度",
"Blend Function": "混合模式",
"LUT Color filter":"LUT 颜色滤镜",
"Intensity":"强度",
"Afterimage":"运动残影",
@ -245,6 +317,7 @@ export default {
"Focus": "焦距",
"Aperture": "孔径",
"MaxBlur": "最大模糊",
"Resolution Scale": "分辨率缩放",
"Pixelate": "像素风",
"PixelSize": "像素大小",
"NormalEdgeStrength": "法向边缘强度",
@ -747,6 +820,7 @@ export default {
Open: "开启",
Close: "关闭",
Enable: "启用",
Enabled: "启用",
Minimum: "最小值",
Maximum: "最大值",
Width: "宽度",
@ -1255,4 +1329,4 @@ export default {
"Camera": '相机',
}
}
};
};

View File

@ -0,0 +1,48 @@
import { t } from "@/language";
// Blend mode options for post-processing blendFunction config.
const BLEND_FUNCTION_KEYS = [
"NORMAL",
"ADD",
"ALPHA",
"AVERAGE",
"COLOR",
"COLOR_BURN",
"COLOR_DODGE",
"DARKEN",
"DIFFERENCE",
"DIVIDE",
"DST",
"EXCLUSION",
"HARD_LIGHT",
"HARD_MIX",
"HUE",
"INVERT",
"INVERT_RGB",
"LIGHTEN",
"LINEAR_BURN",
"LINEAR_DODGE",
"LINEAR_LIGHT",
"LUMINOSITY",
"MULTIPLY",
"NEGATION",
"OVERLAY",
"PIN_LIGHT",
"REFLECT",
"SATURATION",
"SCREEN",
"SET",
"SOFT_LIGHT",
"SRC",
"SUBTRACT",
"VIVID_LIGHT",
] as const;
export const blendFunctionOptions = BLEND_FUNCTION_KEYS.map(k => ({ label: k, value: k }));
// Glitch mode options.
export const glitchModeOptions = [
{ label: t("layout.sider.postProcessing.Sporadic"), value: "SPORADIC" },
{ label: t("layout.sider.postProcessing.Constant Mild"), value: "CONSTANT_MILD" },
{ label: t("layout.sider.postProcessing.Constant Wild"), value: "CONSTANT_WILD" },
];

View File

@ -1,37 +1,122 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {CaretForwardOutline} from "@vicons/ionicons5";
import {t} from "@/language";
import {App} from "@astral3d/engine";
import OutLine from './effect/Sidebar.Effect.Outline.vue';
import FXAA from './effect/Sidebar.Effect.FXAA.vue';
import UnrealBloom from './effect/Sidebar.Effect.UnrealBloom.vue';
import LUT from './effect/Sidebar.Effect.LUT.vue';
import Afterimage from './effect/Sidebar.Effect.Afterimage.vue';
import Bokeh from './effect/Sidebar.Effect.Bokeh.vue';
import Pixelate from './effect/Sidebar.Effect.Pixelate.vue';
import Halftone from './effect/Sidebar.Effect.Halftone.vue';
import EsTip from "@/components/es/EsTip.vue";
import { onMounted, ref } from "vue";
import { CaretForwardOutline } from "@vicons/ionicons5";
import { t } from "@/language";
import { App } from "@astral3d/engine";
import OutLine from "./effect/Sidebar.Effect.Outline.vue";
import SMAA from "./effect/Sidebar.Effect.SMAA.vue";
import SSAO from "./effect/Sidebar.Effect.SSAO.vue";
import UnrealBloom from "./effect/Sidebar.Effect.UnrealBloom.vue";
import Bokeh from "./effect/Sidebar.Effect.Bokeh.vue";
import Pixelate from "./effect/Sidebar.Effect.Pixelate.vue";
import TiltShift from "./effect/Sidebar.Effect.TiltShift.vue";
import Scanline from "./effect/Sidebar.Effect.Scanline.vue";
import BrightnessContrast from "./effect/Sidebar.Effect.BrightnessContrast.vue";
import ChromaticAberration from "./effect/Sidebar.Effect.ChromaticAberration.vue";
import ColorDepth from "./effect/Sidebar.Effect.ColorDepth.vue";
import Glitch from "./effect/Sidebar.Effect.Glitch.vue";
import HueSaturation from "./effect/Sidebar.Effect.HueSaturation.vue";
import LensDistortion from "./effect/Sidebar.Effect.LensDistortion.vue";
import ShockWave from "./effect/Sidebar.Effect.ShockWave.vue";
import ToneMapping from "./effect/Sidebar.Effect.ToneMapping.vue";
import Vignette from "./effect/Sidebar.Effect.Vignette.vue";
import EsTip from "@/components/es/EsTip.vue";
const effectEnabled = ref(App.project.getKey("effect.enabled"));
const effectDefaults: Record<string, any> = {
ToneMapping: { mode: "ACES_FILMIC", exposure: 1.0, blendFunction: "NORMAL" },
SMAA: { enabled: true, preset: "ULTRA" },
SSAO: {
enabled: true,
blendFunction: "MULTIPLY",
samples: 9,
rings: 7,
radius: 0.1825,
intensity: 1.0,
bias: 0.025,
fade: 0.01,
luminanceInfluence: 0.7,
minRadiusScale: 0.1,
depthAwareUpsampling: true,
resolutionScale: 0.5,
distanceThreshold: 0.97,
distanceFalloff: 0.03,
rangeThreshold: 0.0005,
rangeFalloff: 0.001,
worldDistanceThreshold: null,
worldDistanceFalloff: null,
worldProximityThreshold: null,
worldProximityFalloff: null,
colorEnabled: false,
color: "#000000",
},
TiltShift: { enabled: false, offset: 0.0, rotation: 0.0, focusArea: 0.4, feather: 0.3, blendFunction: "NORMAL" },
Scanline: { enabled: false, density: 1.25, scrollSpeed: 0.0, blendFunction: "OVERLAY" },
BrightnessContrast: { enabled: false, brightness: 0.0, contrast: 0.0, blendFunction: "SRC" },
ChromaticAberration: {
enabled: false,
offset: { x: 0.002, y: 0.002 },
radialModulation: false,
modulationOffset: 0.15,
blendFunction: "NORMAL",
},
ColorDepth: { enabled: false, bits: 16, blendFunction: "NORMAL" },
Glitch: {
enabled: false,
chromaticAberrationOffset: null,
delay: { min: 1.5, max: 3.5 },
duration: { min: 0.6, max: 1.0 },
strength: { min: 0.3, max: 1.0 },
mode: "SPORADIC",
ratio: 0.85,
blendFunction: "NORMAL",
},
HueSaturation: { enabled: false, hue: 0.0, saturation: 0.0, blendFunction: "SRC" },
LensDistortion: {
enabled: false,
distortion: { x: 0.0, y: 0.0 },
principalPoint: { x: 0.0, y: 0.0 },
focalLength: { x: 1.0, y: 1.0 },
skew: 0.0,
},
ShockWave: { enabled: false, amplitude: 0.05, waveSize: 0.2, speed: 2.0, maxRadius: 1.0, clickTrigger: true },
Vignette: { enabled: false, offset: 0.5, darkness: 0.5, blendFunction: "NORMAL" },
};
onMounted(() => {
const viewerLoaded = () => {
effectEnabled.value = App.project.getKey("effect.enabled");
// effect setup JSON.parse(undefined)
function ensureEffectDefaults() {
if (App.project.getKey("effect") === undefined) {
App.project.setKey("effect", { enabled: false }, false);
}
window.viewer.removeEventListener('loaded', viewerLoaded);
Object.entries(effectDefaults).forEach(([name, config]) => {
const key = `effect.${name}`;
if (App.project.getKey(key) === undefined) {
App.project.setKey(key, JSON.parse(JSON.stringify(config)), false);
}
});
}
window.viewer.addEventListener('loaded', viewerLoaded);
})
function handleEffectEnabledChange(value:boolean){
App.project.setKey("effect.enabled",value);
}
ensureEffectDefaults();
const effectEnabled = ref(App.project.getKey("effect.enabled"));
onMounted(() => {
const viewerLoaded = () => {
effectEnabled.value = App.project.getKey("effect.enabled");
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleEffectEnabledChange(value: boolean) {
App.project.setKey("effect.enabled", value);
}
</script>
<template>
<div class="flex items-center justify-between">
<h4>{{ t('layout.sider.Post processing') }}</h4>
<h4>{{ t("layout.sider.Post processing") }}</h4>
<n-switch v-model:value="effectEnabled" @update:value="handleEffectEnabledChange">
<template #checked>
{{ t("other.Open") }}
@ -44,7 +129,12 @@ function handleEffectEnabledChange(value:boolean){
<n-divider class="!my-3" />
<n-collapse display-directive="show" :default-expanded-names="['Anti-aliasing','Outline','UnrealBloom']">
<!-- ToneMapping:色调映射 -->
<div class="px-2 mb-3">
<ToneMapping :effect-enabled="effectEnabled" />
</div>
<n-collapse display-directive="show" :default-expanded-names="['Anti-aliasing', 'Outline']">
<template #arrow>
<n-icon>
<CaretForwardOutline />
@ -53,10 +143,14 @@ function handleEffectEnabledChange(value:boolean){
<!-- 抗锯齿 -->
<n-collapse-item :title="t('layout.sider.postProcessing[\'Anti-aliasing\']')" name="Anti-aliasing">
<FXAA :effect-enabled="effectEnabled" />
<SMAA :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- Outline:描边线 -->
<n-collapse-item :title="t('layout.sider.postProcessing.SSAO')" name="SSAO">
<SSAO :effect-enabled="effectEnabled" />
</n-collapse-item>
<n-collapse-item :title="t('layout.sider.postProcessing.Outline')" name="Outline">
<OutLine :effect-enabled="effectEnabled" />
</n-collapse-item>
@ -66,40 +160,74 @@ function handleEffectEnabledChange(value:boolean){
<UnrealBloom :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- LUT:颜色滤镜 -->
<n-collapse-item :title="t('layout.sider.postProcessing.LUT Color filter')" name="LUT">
<LUT :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- 运动残影 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Afterimage')" name="Afterimage">
<Afterimage :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- Bokeh:变焦 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Bokeh')" name="Bokeh">
<Bokeh :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- Pixelate:像素风 -->
<n-collapse-item name="Pixelate">
<n-collapse-item :title="t(`layout.sider.postProcessing.Pixelate`)" name="Pixelate">
<Pixelate :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- TiltShift:移轴模糊 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Tilt Shift')" name="TiltShift">
<TiltShift :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- Scanline:扫描线 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Scanline')" name="Scanline">
<Scanline :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- BrightnessContrast:亮度对比度 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Brightness Contrast')" name="BrightnessContrast">
<BrightnessContrast :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- ChromaticAberration:色差 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Chromatic Aberration')" name="ChromaticAberration">
<ChromaticAberration :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- ColorDepth:色深 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Color Depth')" name="ColorDepth">
<ColorDepth :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- Glitch:故障 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Glitch')" name="Glitch">
<Glitch :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- HueSaturation:色相饱和度 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Hue Saturation')" name="HueSaturation">
<HueSaturation :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- LensDistortion:镜头畸变 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Lens Distortion')" name="LensDistortion">
<LensDistortion :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- ShockWave:冲击波 -->
<n-collapse-item name="ShockWave">
<template #header>
<n-text>
<EsTip class="!justify-start" :content="t(`layout.sider.postProcessing.Pixelate`)">
{{ t(`layout.sider.postProcessing.Use a solid color background to achieve the best rendering effect`) }}
<EsTip class="!justify-start" :content="t(`layout.sider.postProcessing.Shock Wave`)">
{{ t(`layout.sider.postProcessing.Click on scene to trigger shock wave effect`) }}
</EsTip>
</n-text>
</template>
<Pixelate :effect-enabled="effectEnabled" />
<ShockWave :effect-enabled="effectEnabled" />
</n-collapse-item>
<!-- Halftone:半色调 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Halftoning')" name="Halftoning">
<Halftone :effect-enabled="effectEnabled" />
<!-- Vignette:晕影 -->
<n-collapse-item :title="t('layout.sider.postProcessing.Vignette')" name="Vignette">
<Vignette :effect-enabled="effectEnabled" />
</n-collapse-item>
</n-collapse>
</template>
<style lang="less">
</style>
<style lang="less"></style>

View File

@ -28,7 +28,7 @@ function handleAfterimageConfigChange(){
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enable`) }}</span>
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="afterimage.enabled" :disabled="!effectEnabled" @update:checked="handleAfterimageConfigChange"/>
</div>
@ -45,4 +45,5 @@ function handleAfterimageConfigChange(){
<style scoped lang="less">
</style>
</style>

View File

@ -28,7 +28,7 @@ function handleBokehConfigChange(){
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enable`) }}</span>
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="bokeh.enabled" :disabled="!effectEnabled" @update:checked="handleBokehConfigChange"/>
</div>
@ -62,4 +62,5 @@ function handleBokehConfigChange(){
<style scoped lang="less">
</style>
</style>

View File

@ -0,0 +1,67 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const bc = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.BrightnessContrast"))));
const disabled = computed(() => !props.effectEnabled || !bc.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(bc, App.project.getKey("effect.BrightnessContrast"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.BrightnessContrast`, toRaw(bc));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="bc.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 亮度 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Brightness`) }}</span>
<div>
<n-slider v-model:value="bc.brightness" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 对比度 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Contrast`) }}</span>
<div>
<n-slider v-model:value="bc.contrast" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 混合模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select size="small" v-model:value="bc.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -0,0 +1,83 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const ca = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.ChromaticAberration"))));
const disabled = computed(() => !props.effectEnabled || !ca.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(ca, App.project.getKey("effect.ChromaticAberration"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.ChromaticAberration`, toRaw(ca));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="ca.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 偏移 X -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Offset X`) }}</span>
<div>
<n-slider v-model:value="ca.offset.x" :step="0.001" :min="0" :max="0.05" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 偏移 Y -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Offset Y`) }}</span>
<div>
<n-slider v-model:value="ca.offset.y" :step="0.001" :min="0" :max="0.05" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 径向调制 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Radial Modulation`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="ca.radialModulation" :disabled="disabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 调制偏移 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Modulation Offset`) }}</span>
<div>
<n-slider v-model:value="ca.modulationOffset" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 混合模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select size="small" v-model:value="ca.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const colorDepth = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.ColorDepth"))));
const disabled = computed(() => !props.effectEnabled || !colorDepth.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(colorDepth, App.project.getKey("effect.ColorDepth"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.ColorDepth`, toRaw(colorDepth));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="colorDepth.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 色深位数 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Bits`) }}</span>
<div>
<n-slider v-model:value="colorDepth.bits" :step="1" :min="1" :max="32" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 混合模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select v-model:value="colorDepth.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -27,7 +27,7 @@ function handleFXAAConfigChange(){
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enable`) }}</span>
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="faxx.enabled" :disabled="!effectEnabled" @update:checked="handleFXAAConfigChange"/>
</div>
@ -36,4 +36,5 @@ function handleFXAAConfigChange(){
<style scoped lang="less">
</style>
</style>

View File

@ -0,0 +1,121 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions, glitchModeOptions } from "../../../../../utils/common/postprocessing";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const glitch = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.Glitch"))));
const disabled = computed(() => !props.effectEnabled || !glitch.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(glitch, App.project.getKey("effect.Glitch"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.Glitch`, toRaw(glitch));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="glitch.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 故障模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Glitch Mode`) }}</span>
<div>
<n-select v-model:value="glitch.mode" :options="glitchModeOptions" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 延迟最小值 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Delay Min`) }}</span>
<div>
<n-slider v-model:value="glitch.delay.min" :step="0.1" :min="0" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 延迟最大值 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Delay Max`) }}</span>
<div>
<n-slider v-model:value="glitch.delay.max" :step="0.1" :min="0" :max="10" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 持续时间最小值 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Duration Min`) }}</span>
<div>
<n-slider v-model:value="glitch.duration.min" :step="0.1" :min="0" :max="2" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 持续时间最大值 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Duration Max`) }}</span>
<div>
<n-slider v-model:value="glitch.duration.max" :step="0.1" :min="0" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 强度最小值 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Strength Min`) }}</span>
<div>
<n-slider v-model:value="glitch.strength.min" :step="0.1" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 强度最大值 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Strength Max`) }}</span>
<div>
<n-slider v-model:value="glitch.strength.max" :step="0.1" :min="0" :max="2" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 故障比率 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Ratio`) }}</span>
<div>
<n-slider v-model:value="glitch.ratio" :step="0.05" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 混合模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select
size="small"
v-model:value="glitch.blendFunction"
:options="blendFunctionOptions"
:disabled="disabled"
@update:value="handleConfigChange"
/>
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -28,7 +28,7 @@ function handleHalftoneConfigChange(){
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enable`) }}</span>
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="halftone.enabled" :disabled="!effectEnabled" @update:checked="handleHalftoneConfigChange"/>
</div>
@ -120,4 +120,5 @@ function handleHalftoneConfigChange(){
<style scoped lang="less">
</style>
</style>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const hueSat = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.HueSaturation"))));
const disabled = computed(() => !props.effectEnabled || !hueSat.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(hueSat, App.project.getKey("effect.HueSaturation"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.HueSaturation`, toRaw(hueSat));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="hueSat.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 色相 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Hue`) }}</span>
<div>
<n-slider v-model:value="hueSat.hue" :step="0.01" :min="-3.14" :max="3.14" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 饱和度 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Saturation`) }}</span>
<div>
<n-slider v-model:value="hueSat.saturation" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 混合模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select v-model:value="hueSat.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -30,7 +30,7 @@ function handleLUTConfigChange(){
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enable`) }}</span>
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="lut.enabled" :disabled="!effectEnabled" @update:checked="handleLUTConfigChange"/>
</div>
@ -55,4 +55,5 @@ function handleLUTConfigChange(){
<style scoped lang="less">
</style>
</style>

View File

@ -0,0 +1,98 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const lens = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.LensDistortion"))));
const disabled = computed(() => !props.effectEnabled || !lens.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(lens, App.project.getKey("effect.LensDistortion"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.LensDistortion`, toRaw(lens));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="lens.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 畸变 X -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Distortion X`) }}</span>
<div>
<n-slider v-model:value="lens.distortion.x" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 畸变 Y -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Distortion Y`) }}</span>
<div>
<n-slider v-model:value="lens.distortion.y" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 主点 X -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Principal Point X`) }}</span>
<div>
<n-slider v-model:value="lens.principalPoint.x" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 主点 Y -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Principal Point Y`) }}</span>
<div>
<n-slider v-model:value="lens.principalPoint.y" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 焦距 X -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Focal Length X`) }}</span>
<div>
<n-slider v-model:value="lens.focalLength.x" :step="0.1" :min="0.1" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 焦距 Y -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Focal Length Y`) }}</span>
<div>
<n-slider v-model:value="lens.focalLength.y" :step="0.1" :min="0.1" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 倾斜 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Skew`) }}</span>
<div>
<n-slider v-model:value="lens.skew" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -28,7 +28,7 @@ function handleOutlineConfigChange(){
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enable`) }}</span>
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="outline.enabled" :disabled="!effectEnabled" @update:checked="handleOutlineConfigChange"/>
</div>
@ -85,4 +85,5 @@ function handleOutlineConfigChange(){
<style scoped lang="less">
</style>
</style>

View File

@ -28,7 +28,7 @@ function handlePixelateConfigChange(){
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enable`) }}</span>
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="pixelate.enabled" :disabled="!effectEnabled" @update:checked="handlePixelateConfigChange"/>
</div>
@ -61,4 +61,5 @@ function handlePixelateConfigChange(){
<style scoped lang="less">
</style>
</style>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
// SMAA (Subpixel Morphological Anti-Aliasing) FXAA
const smaa = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.SMAA"))));
const disabled = computed(() => !props.effectEnabled || !smaa.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(smaa, App.project.getKey("effect.SMAA"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleSMAAConfigChange() {
App.project.setKey(`effect.SMAA`, toRaw(smaa));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="smaa.enabled" :disabled="!effectEnabled" @update:checked="handleSMAAConfigChange" />
</div>
</div>
<!-- 质量预设 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Quality Preset`) }}</span>
<div>
<n-select
v-model:value="smaa.preset"
:disabled="disabled"
@update:value="handleSMAAConfigChange"
:options="[
{ label: 'Low', value: 'LOW' },
{ label: 'Medium', value: 'MEDIUM' },
{ label: 'High', value: 'HIGH' },
{ label: 'Ultra', value: 'ULTRA' },
]"
/>
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -0,0 +1,208 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const ssao = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.SSAO"))));
const disabled = computed(() => !props.effectEnabled || !ssao.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(ssao, App.project.getKey("effect.SSAO"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
// NaN
function normalizeOptionalNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function handleConfigChange() {
ssao.worldDistanceThreshold = normalizeOptionalNumber(ssao.worldDistanceThreshold);
ssao.worldDistanceFalloff = normalizeOptionalNumber(ssao.worldDistanceFalloff);
ssao.worldProximityThreshold = normalizeOptionalNumber(ssao.worldProximityThreshold);
ssao.worldProximityFalloff = normalizeOptionalNumber(ssao.worldProximityFalloff);
App.project.setKey("effect.SSAO", toRaw(ssao));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="ssao.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select size="small" v-model:value="ssao.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Samples`) }}</span>
<div>
<n-slider v-model:value="ssao.samples" :step="1" :min="1" :max="64" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Rings`) }}</span>
<div>
<n-slider v-model:value="ssao.rings" :step="1" :min="1" :max="32" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Radius`) }}</span>
<div>
<n-slider v-model:value="ssao.radius" :step="0.001" :min="0.01" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Strength`) }}</span>
<div>
<n-slider v-model:value="ssao.intensity" :step="0.01" :min="0" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Bias`) }}</span>
<div>
<n-slider v-model:value="ssao.bias" :step="0.001" :min="0" :max="0.1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Fade`) }}</span>
<div>
<n-slider v-model:value="ssao.fade" :step="0.001" :min="0" :max="0.1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Luminance Influence`) }}</span>
<div>
<n-slider v-model:value="ssao.luminanceInfluence" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Min Radius Scale`) }}</span>
<div>
<n-slider v-model:value="ssao.minRadiusScale" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Depth-aware Upsampling`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="ssao.depthAwareUpsampling" :disabled="disabled" @update:checked="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Resolution Scale`) }}</span>
<div>
<n-slider v-model:value="ssao.resolutionScale" :step="0.05" :min="0.1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Distance Threshold`) }}</span>
<div>
<n-slider v-model:value="ssao.distanceThreshold" :step="0.001" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Distance Falloff`) }}</span>
<div>
<n-slider v-model:value="ssao.distanceFalloff" :step="0.001" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Range Threshold`) }}</span>
<div>
<n-slider v-model:value="ssao.rangeThreshold" :step="0.0001" :min="0" :max="0.02" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Range Falloff`) }}</span>
<div>
<n-slider v-model:value="ssao.rangeFalloff" :step="0.0001" :min="0" :max="0.02" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.World Distance Threshold`) }}</span>
<div>
<n-input-number v-model:value="ssao.worldDistanceThreshold" size="small" :min="0" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.World Distance Falloff`) }}</span>
<div>
<n-input-number v-model:value="ssao.worldDistanceFalloff" size="small" :min="0" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.World Proximity Threshold`) }}</span>
<div>
<n-input-number v-model:value="ssao.worldProximityThreshold" size="small" :min="0" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.World Proximity Falloff`) }}</span>
<div>
<n-input-number v-model:value="ssao.worldProximityFalloff" size="small" :min="0" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Colorize`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="ssao.colorEnabled" :disabled="disabled" @update:checked="handleConfigChange" />
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.AO Color`) }}</span>
<div>
<n-color-picker
v-model:value="ssao.color"
:show-alpha="false"
:modes="['hex']"
size="small"
:disabled="disabled || !ssao.colorEnabled"
@update:value="handleConfigChange"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,75 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const scanline = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.Scanline"))));
const disabled = computed(() => !props.effectEnabled || !scanline.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(scanline, App.project.getKey("effect.Scanline"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.Scanline`, toRaw(scanline));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="scanline.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 密度 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Density`) }}</span>
<div>
<n-slider v-model:value="scanline.density" :step="0.05" :min="0.1" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 滚动速度 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Scroll Speed`) }}</span>
<div>
<n-slider v-model:value="scanline.scrollSpeed" :step="0.01" :min="0" :max="2" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 混合模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select
size="small"
v-model:value="scanline.blendFunction"
:options="blendFunctionOptions"
:disabled="disabled"
@update:value="handleConfigChange"
/>
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const shockWave = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.ShockWave"))));
const disabled = computed(() => !props.effectEnabled || !shockWave.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(shockWave, App.project.getKey("effect.ShockWave"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.ShockWave`, toRaw(shockWave));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="shockWave.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 振幅 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Amplitude`) }}</span>
<div>
<n-slider v-model:value="shockWave.amplitude" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 波浪大小 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Wave Size`) }}</span>
<div>
<n-slider v-model:value="shockWave.waveSize" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 速度 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Speed`) }}</span>
<div>
<n-slider v-model:value="shockWave.speed" :step="0.1" :min="0.1" :max="10" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 最大半径 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Max Radius`) }}</span>
<div>
<EsInputNumber
v-model:value="shockWave.maxRadius"
:show-button="false"
:decimal="1"
:min="0"
:max="100"
size="small"
:disabled="disabled"
@change="handleConfigChange"
/>
</div>
</div>
<!-- 点击触发 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Click Trigger`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="shockWave.clickTrigger" :disabled="disabled" @update:checked="handleConfigChange" />
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
const props = withDefaults(
defineProps<{
effectEnabled: boolean;
}>(),
{
effectEnabled: false,
}
);
const tiltShift = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.TiltShift"))));
const disabled = computed(() => !props.effectEnabled || !tiltShift.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(tiltShift, App.project.getKey("effect.TiltShift"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.TiltShift`, toRaw(tiltShift));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="tiltShift.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 焦点偏移 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Offset`) }}</span>
<div>
<n-slider v-model:value="tiltShift.offset" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 旋转角度 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Rotation`) }}</span>
<div>
<n-slider v-model:value="tiltShift.rotation" :step="0.01" :min="0" :max="6.28" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 焦点区域 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Focus Area`) }}</span>
<div>
<n-slider v-model:value="tiltShift.focusArea" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 羽化 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Feather`) }}</span>
<div>
<n-slider v-model:value="tiltShift.feather" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 混合模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select
size="small"
v-model:value="tiltShift.blendFunction"
:options="blendFunctionOptions"
:disabled="disabled"
@update:value="handleConfigChange"
/>
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import { h, onMounted, reactive, toRaw, VNode } from "vue";
import { SelectOption, NTooltip } from "naive-ui";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
const defaultToneMapping = {
mode: "ACES_FILMIC",
exposure: 1,
blendFunction: "NORMAL",
};
const toneMapping = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.ToneMapping") || defaultToneMapping)));
//
const toneMappingModeOptions = [
{ label: "Linear", value: "LINEAR", tooltip: "线性映射,保持原始颜色" },
{ label: "Reinhard", value: "REINHARD", tooltip: "Reinhard 映射,柔和的高光压缩" },
{ label: "Reinhard2", value: "REINHARD2", tooltip: "改进的 Reinhard 映射" },
{ label: "Optimized Cineon", value: "OPTIMIZED_CINEON", tooltip: "优化的电影风格映射" },
{ label: "ACES Filmic", value: "ACES_FILMIC", tooltip: "电影工业标准 ACES 映射" },
{ label: "AgX", value: "AGX", tooltip: "AgX 映射,适合户外场景" },
{ label: "Neutral", value: "NEUTRAL", tooltip: "中性映射,平衡的色调" },
];
const renderToneMappingOption = ({ node, option }: { node: VNode; option: SelectOption }) => {
return h(NTooltip, null, {
trigger: () => node,
default: () => (option as any).tooltip || option.label,
});
};
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(toneMapping, App.project.getKey("effect.ToneMapping") || defaultToneMapping);
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.ToneMapping`, toRaw(toneMapping));
}
</script>
<template>
<!-- 色调映射模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.rendererConfig.Tone mapping`) || "色调映射" }}</span>
<div>
<n-select
v-model:value="toneMapping.mode"
:options="toneMappingModeOptions"
:render-option="renderToneMappingOption"
size="small"
@update:value="handleConfigChange"
/>
</div>
</div>
<!-- 曝光度 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Exposure`) || "曝光度" }}</span>
<div>
<n-slider v-model:value="toneMapping.exposure" :step="0.01" :min="0.1" :max="3" @update:value="handleConfigChange" />
</div>
</div>
<!-- 混合模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select size="small" v-model:value="toneMapping.blendFunction" :options="blendFunctionOptions" @update:value="handleConfigChange" />
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -28,7 +28,7 @@ function handleUnrealBloomConfigChange(){
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enable`) }}</span>
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="unrealBloom.enabled" :disabled="!effectEnabled" @update:checked="handleUnrealBloomConfigChange"/>
</div>
@ -61,4 +61,5 @@ function handleUnrealBloomConfigChange(){
<style scoped lang="less">
</style>
</style>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { computed, onMounted, reactive, toRaw } from "vue";
import { t } from "@/language";
import { App, Utils } from "@astral3d/engine";
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
defineProps<{
effectEnabled: boolean;
}>();
const vignette = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.Vignette"))));
const disabled = computed(() => !vignette.enabled);
onMounted(() => {
const viewerLoaded = () => {
Utils.deepAssign(vignette, App.project.getKey("effect.Vignette"));
window.viewer.removeEventListener("loaded", viewerLoaded);
};
window.viewer.addEventListener("loaded", viewerLoaded);
});
function handleConfigChange() {
App.project.setKey(`effect.Vignette`, toRaw(vignette));
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enabled`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="vignette.enabled" @update:checked="handleConfigChange" />
</div>
</div>
<!-- 偏移 -->
<div class="sidebar-config-item">
<span>{{ t("layout.sider.postProcessing.Offset") }}</span>
<div>
<n-slider v-model:value="vignette.offset" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 暗度 -->
<div class="sidebar-config-item">
<span>{{ t("layout.sider.postProcessing.Darkness") }}</span>
<div>
<n-slider v-model:value="vignette.darkness" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
</div>
</div>
<!-- 混合模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
<div>
<n-select
size="small"
v-model:value="vignette.blendFunction"
:options="blendFunctionOptions"
:disabled="disabled"
@update:value="handleConfigChange"
/>
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@ -41,6 +41,46 @@ export const defaultProjectInfo = (): IAppProject.Info => ({
// 后处理
effect: {
enabled: false,
// 色调映射 (ToneMappingEffect)
ToneMapping: {
// 色调映射模式LINEAR, REINHARD, REINHARD2, OPTIMIZED_CINEON, ACES_FILMIC, AGX, NEUTRAL
mode: "ACES_FILMIC",
// 曝光度
exposure: 1.0,
// 混合模式
blendFunction: "NORMAL"
},
// 抗锯齿 (SMAAEffect)
SMAA: {
enabled: true,
// 质量预设: LOW, MEDIUM, HIGH, ULTRA
preset: "ULTRA"
},
// 环境光遮蔽 (SSAOEffect)
SSAO: {
enabled: true,
blendFunction: "MULTIPLY",
samples: 9,
rings: 7,
radius: 0.1825,
intensity: 1.0,
bias: 0.025,
fade: 0.01,
luminanceInfluence: 0.7,
minRadiusScale: 0.1,
depthAwareUpsampling: true,
resolutionScale: 0.5,
distanceThreshold: 0.97,
distanceFalloff: 0.03,
rangeThreshold: 0.0005,
rangeFalloff: 0.001,
worldDistanceThreshold: null,
worldDistanceFalloff: null,
worldProximityThreshold: null,
worldProximityFalloff: null,
colorEnabled: false,
color: "#000000",
},
// 描边线
Outline: {
enabled: true,
@ -57,7 +97,14 @@ export const defaultProjectInfo = (): IAppProject.Info => ({
// 可见边缘的颜色
visibleEdgeColor: "#ffee00",
// 不可见边缘的颜色
hiddenEdgeColor: "#ff6a00"
hiddenEdgeColor: "#ff6a00",
// Astral postprocessing 兼容字段
pulseSpeed: 0.0,
xRay: true,
blur: true,
kernelSize: 1,
multisampling: 4,
blendFunction: "SCREEN"
},
// 抗锯齿
FXAA: {
@ -71,7 +118,14 @@ export const defaultProjectInfo = (): IAppProject.Info => ({
// 光晕强度
strength: 1,
// 光晕半径
radius: 0
radius: 0,
// Astral postprocessing 兼容字段
intensity: 1.0,
luminanceThreshold: 0.9,
luminanceSmoothing: 0.025,
levels: 8,
mipmapBlur: true,
blendFunction: "SCREEN"
},
// 背景虚化
Bokeh: {
@ -81,7 +135,12 @@ export const defaultProjectInfo = (): IAppProject.Info => ({
// 孔径,类似相机孔径调节
aperture: 0.00005,
// 最大模糊程度
maxblur: 0.01
maxblur: 0.01,
// Astral postprocessing 兼容字段
focusDistance: 10.0,
focusRange: 5.0,
bokehScale: 2.0,
resolutionScale: 0.5
},
// 像素风
Pixelate: {
@ -92,6 +151,87 @@ export const defaultProjectInfo = (): IAppProject.Info => ({
normalEdgeStrength: 0.3,
// 深度边缘强度
depthEdgeStrength: 0.4,
// Astral postprocessing 兼容字段
granularity: 6,
},
// 移轴模糊 (TiltShiftEffect)
TiltShift: {
enabled: false,
offset: 0.0,
rotation: 0.0,
focusArea: 0.4,
feather: 0.3,
blendFunction: "NORMAL"
},
// 扫描线 (ScanlineEffect)
Scanline: {
enabled: false,
density: 1.25,
scrollSpeed: 0.0,
blendFunction: "OVERLAY"
},
// 亮度对比度 (BrightnessContrastEffect)
BrightnessContrast: {
enabled: false,
brightness: 0.0,
contrast: 0.0,
blendFunction: "SRC"
},
// 色差 (ChromaticAberrationEffect)
ChromaticAberration: {
enabled: false,
offset: { x: 0.002, y: 0.002 },
radialModulation: false,
modulationOffset: 0.15,
blendFunction: "NORMAL"
},
// 色深 (ColorDepthEffect)
ColorDepth: {
enabled: false,
bits: 16,
blendFunction: "NORMAL"
},
// 故障 (GlitchEffect)
Glitch: {
enabled: false,
chromaticAberrationOffset: null,
delay: { min: 1.5, max: 3.5 },
duration: { min: 0.6, max: 1.0 },
strength: { min: 0.3, max: 1.0 },
mode: "SPORADIC",
ratio: 0.85,
blendFunction: "NORMAL"
},
// 色相饱和度 (HueSaturationEffect)
HueSaturation: {
enabled: false,
hue: 0.0,
saturation: 0.0,
blendFunction: "SRC"
},
// 镜头畸变 (LensDistortionEffect)
LensDistortion: {
enabled: false,
distortion: { x: 0.0, y: 0.0 },
principalPoint: { x: 0.0, y: 0.0 },
focalLength: { x: 1.0, y: 1.0 },
skew: 0.0
},
// 冲击波 (ShockWaveEffect)
ShockWave: {
enabled: false,
amplitude: 0.05,
waveSize: 0.2,
speed: 2.0,
maxRadius: 1.0,
clickTrigger: true
},
// 暗角 (VignetteEffect)
Vignette: {
enabled: false,
offset: 0.5,
darkness: 0.5,
blendFunction: "NORMAL"
},
// 半色调
Halftone: {
@ -223,7 +363,12 @@ class Project {
* @param {string} key .a.b.c
*/
getKey(key: string): any {
return getNestedProperty(this.info, key);
const value = getNestedProperty(this.info, key);
if (value !== undefined) {
return value;
}
return getNestedProperty(defaultProjectInfo(), key);
}
/**