Files
eacp_webapp/src/Map/Cesium_Render_Settings.tsx
T
2026-07-27 16:43:40 +08:00

358 lines
18 KiB
TypeScript

import React from "react";
import {Button, InputNumber, message, Space, Switch, Tooltip} from "antd";
import {QuestionCircleOutlined} from "@ant-design/icons";
import axios from "axios";
import enhance from "../core/enhance.tsx";
import {col_style, row_style, setting_style} from "../Global.tsx";
export type Cesium_Graphics_Config = {
// 显示 Cesium FPS/帧耗时诊断面板,不改变画质。
debugShowFramesPerSecond: boolean
// 限制 Cesium 渲染循环目标帧率,降低上限可减少 GPU 占用。
targetFrameRate: number
// 使用浏览器推荐分辨率,避免高 DPI 屏幕按物理像素渲染。
useBrowserRecommendedResolution: boolean
// WebGL 内部渲染分辨率缩放,越低越省 GPU 但越模糊。
resolutionScale: number
// MSAA 多重采样数量,1 表示关闭几何抗锯齿。
msaaSamples: number
// FXAA 后处理抗锯齿,开销低但可能让文字和细线变软。
fxaa: boolean
// 场景阴影开关,大量模型或地形开启后 GPU 开销明显。
shadows: boolean
// 地球光照开关,开启后地表按光源方向产生明暗。
enableLighting: boolean
// 真实日照模式,使用太阳方向、系统时间和动态大气表达昼夜状态。
solarLighting: boolean
// 真实日照模式下的太阳光强度,只影响视觉明暗。
solarLightIntensity: number
// 地球瓦片屏幕空间误差,值越大越快但地面细节越粗。
maximumScreenSpaceError: number
// 仅 3D 模式加载 Terrarium 真实地形,2D/2.5D 不加载。
terrainEnabledIn3D: boolean
// 地形垂直起伏倍率,只改变视觉起伏,不增加高程精度。
terrainExaggeration: number
// 限制 Terrarium 地形最高请求层级,减少无效子瓦片请求。
terrainLimitMaximumLevel: boolean
// 启用层级限制时允许请求的最高地形层级。
terrainMaximumLevel: number
// 已解码地形高度瓦片缓存数量,越大越占内存但回看更快。
terrainCacheTiles: number
}
export const default_cesium_graphics_config: Cesium_Graphics_Config = {
debugShowFramesPerSecond: true,
targetFrameRate: 30,
useBrowserRecommendedResolution: true,
resolutionScale: 0.7,
msaaSamples: 1,
fxaa: false,
shadows: false,
enableLighting: false,
solarLighting: false,
solarLightIntensity: 1.5,
maximumScreenSpaceError: 4,
terrainEnabledIn3D: false,
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 15,
terrainCacheTiles: 128
};
export const cesium_graphics_config_event = "ecap_cesium_graphics_config_changed";
type Number_Config_Key = "targetFrameRate" | "resolutionScale" | "msaaSamples" | "solarLightIntensity" | "maximumScreenSpaceError" | "terrainExaggeration" | "terrainMaximumLevel" | "terrainCacheTiles"
type Boolean_Config_Key = Exclude<keyof Cesium_Graphics_Config, Number_Config_Key>
const graphics_presets: Record<"low" | "medium" | "high", Cesium_Graphics_Config> = {
low: {...default_cesium_graphics_config},
medium: {
debugShowFramesPerSecond: true,
targetFrameRate: 45,
useBrowserRecommendedResolution: true,
resolutionScale: 0.85,
msaaSamples: 1,
fxaa: true,
shadows: false,
enableLighting: false,
solarLighting: false,
solarLightIntensity: 1.5,
maximumScreenSpaceError: 3,
terrainEnabledIn3D: true,
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 15,
terrainCacheTiles: 128
},
high: {
debugShowFramesPerSecond: true,
targetFrameRate: 60,
useBrowserRecommendedResolution: false,
resolutionScale: 1.0,
msaaSamples: 4,
fxaa: true,
shadows: false,
enableLighting: true,
solarLighting: true,
solarLightIntensity: 1.5,
maximumScreenSpaceError: 2,
terrainEnabledIn3D: true,
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 17,
terrainCacheTiles: 256
}
};
const graphics_help: Record<keyof Cesium_Graphics_Config, string[]> = {
debugShowFramesPerSecond: ["显示 FPS、每帧耗时和诊断信息。", "只用于判断卡顿,基本不影响画质。"],
targetFrameRate: ["限制 Cesium 渲染循环的目标帧率。", "越高越流畅,GPU 占用越高;集显建议 30。"],
useBrowserRecommendedResolution: ["开启后按 CSS 像素渲染,忽略高 DPI 物理像素倍率。", "高 DPI 屏幕上通常能显著降低集显压力。"],
resolutionScale: ["调整 WebGL 内部渲染分辨率。", "0.7 约等于渲染 49% 像素;越低越快但更模糊。"],
msaaSamples: ["几何边缘多重采样抗锯齿。", "1 表示关闭;数值越高边缘越平滑,GPU 开销越大。"],
fxaa: ["屏幕空间后处理抗锯齿。", "开销通常低于 MSAA,但可能让文字和细线变软。"],
shadows: ["控制模型、Primitive 和地形阴影。", "画面更真实,但对大量模型和地形开销明显。"],
enableLighting: ["根据场景光源给地球表面做明暗。", "这个开关不驱动系统时间;需要真实昼夜时打开“真实日照”。"],
solarLighting: ["使用 Cesium 太阳方向、系统时间和动态大气表达昼夜。", "地球和地面对象保持地固坐标,不手动旋转地球;需要更真实的时间态势时开启。"],
solarLightIntensity: ["真实日照模式下的太阳光强度。", "数值越大白天越亮、明暗对比越强;不会改变飞机、基站和轨迹数据。"],
maximumScreenSpaceError: ["控制地球瓦片 LOD 的屏幕误差。", "数值越大越快但地面更粗糙;4 是集显平衡档。"],
terrainEnabledIn3D: ["3D 模式下加载 Terrarium 真实高程地形。", "会增加网络、解码、CPU 和 GPU 压力;2D/2.5D 默认不加载。"],
terrainExaggeration: ["控制地形垂直方向的视觉夸张倍率。", "1.0 为真实高度;0.0 压平地形;只改变视觉起伏,不增加高程精度,前端不限制范围。"],
terrainLimitMaximumLevel: ["限制 Terrarium 地形继续请求更深层级。", "只限制高程层级,减少无意义子瓦片请求。"],
terrainMaximumLevel: ["地形允许加载的最高瓦片层级。", "只有启用地形且限制层级时生效;越高越精细也越耗资源。"],
terrainCacheTiles: ["内存中保留的已解码地形瓦片数量。", "越大回看越快但占用更多内存;过小会增加重复请求和解码。"]
};
type Cesium_Graphics_Config_Json = {
debug_show_frames_per_second: boolean
target_frame_rate: number
use_browser_recommended_resolution: boolean
resolution_scale: number
msaa_samples: number
fxaa: boolean
shadows: boolean
enable_lighting: boolean
solar_lighting: boolean
solar_light_intensity: number
maximum_screen_space_error: number
terrain_enabled_in_3d: boolean
terrain_exaggeration: number
terrain_limit_maximum_level: boolean
terrain_maximum_level: number
terrain_cache_tiles: number
}
function number_value(value: unknown, fallback: number, min: number, max: number): number {
const next = Number(value);
if (!Number.isFinite(next)) return fallback;
return Math.min(max, Math.max(min, next));
}
function unrestricted_number_value(value: unknown, fallback: number): number {
const next = Number(value);
return Number.isFinite(next) ? next : fallback;
}
function boolean_value(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
export function normalize_cesium_graphics_config(data: Partial<Cesium_Graphics_Config>): Cesium_Graphics_Config {
return {
debugShowFramesPerSecond: boolean_value(data.debugShowFramesPerSecond, default_cesium_graphics_config.debugShowFramesPerSecond),
targetFrameRate: number_value(data.targetFrameRate, default_cesium_graphics_config.targetFrameRate, 1, 120),
useBrowserRecommendedResolution: boolean_value(data.useBrowserRecommendedResolution, default_cesium_graphics_config.useBrowserRecommendedResolution),
resolutionScale: number_value(data.resolutionScale, default_cesium_graphics_config.resolutionScale, 0.2, 1.5),
msaaSamples: number_value(data.msaaSamples, default_cesium_graphics_config.msaaSamples, 1, 8),
fxaa: boolean_value(data.fxaa, default_cesium_graphics_config.fxaa),
shadows: boolean_value(data.shadows, default_cesium_graphics_config.shadows),
enableLighting: boolean_value(data.enableLighting, default_cesium_graphics_config.enableLighting),
solarLighting: boolean_value(data.solarLighting, default_cesium_graphics_config.solarLighting),
solarLightIntensity: number_value(data.solarLightIntensity, default_cesium_graphics_config.solarLightIntensity, 0, 10),
maximumScreenSpaceError: number_value(data.maximumScreenSpaceError, default_cesium_graphics_config.maximumScreenSpaceError, 1, 16),
terrainEnabledIn3D: boolean_value(data.terrainEnabledIn3D, default_cesium_graphics_config.terrainEnabledIn3D),
terrainExaggeration: unrestricted_number_value(data.terrainExaggeration, default_cesium_graphics_config.terrainExaggeration),
terrainLimitMaximumLevel: boolean_value(data.terrainLimitMaximumLevel, default_cesium_graphics_config.terrainLimitMaximumLevel),
terrainMaximumLevel: number_value(data.terrainMaximumLevel, default_cesium_graphics_config.terrainMaximumLevel, 0, 24),
terrainCacheTiles: Math.round(number_value(data.terrainCacheTiles, default_cesium_graphics_config.terrainCacheTiles, 16, 2048))
};
}
function from_server_config(data: Partial<Cesium_Graphics_Config_Json>): Cesium_Graphics_Config {
return normalize_cesium_graphics_config({
debugShowFramesPerSecond: data.debug_show_frames_per_second,
targetFrameRate: data.target_frame_rate,
useBrowserRecommendedResolution: data.use_browser_recommended_resolution,
resolutionScale: data.resolution_scale,
msaaSamples: data.msaa_samples,
fxaa: data.fxaa,
shadows: data.shadows,
enableLighting: data.enable_lighting,
solarLighting: data.solar_lighting,
solarLightIntensity: data.solar_light_intensity,
maximumScreenSpaceError: data.maximum_screen_space_error,
terrainEnabledIn3D: data.terrain_enabled_in_3d,
terrainExaggeration: data.terrain_exaggeration,
terrainLimitMaximumLevel: data.terrain_limit_maximum_level,
terrainMaximumLevel: data.terrain_maximum_level,
terrainCacheTiles: data.terrain_cache_tiles
});
}
function to_server_config(config: Cesium_Graphics_Config): Cesium_Graphics_Config_Json {
const normalized = normalize_cesium_graphics_config(config);
return {
debug_show_frames_per_second: normalized.debugShowFramesPerSecond,
target_frame_rate: normalized.targetFrameRate,
use_browser_recommended_resolution: normalized.useBrowserRecommendedResolution,
resolution_scale: normalized.resolutionScale,
msaa_samples: normalized.msaaSamples,
fxaa: normalized.fxaa,
shadows: normalized.shadows,
enable_lighting: normalized.enableLighting,
solar_lighting: normalized.solarLighting,
solar_light_intensity: normalized.solarLightIntensity,
maximum_screen_space_error: normalized.maximumScreenSpaceError,
terrain_enabled_in_3d: normalized.terrainEnabledIn3D,
terrain_exaggeration: normalized.terrainExaggeration,
terrain_limit_maximum_level: normalized.terrainLimitMaximumLevel,
terrain_maximum_level: normalized.terrainMaximumLevel,
terrain_cache_tiles: normalized.terrainCacheTiles
};
}
export async function load_cesium_graphics_config(): Promise<Cesium_Graphics_Config> {
const response = await axios.get<Cesium_Graphics_Config_Json>("/map/graphics");
return from_server_config(response.data);
}
export async function save_cesium_graphics_config(config: Cesium_Graphics_Config): Promise<Cesium_Graphics_Config> {
const response = await axios.post<Cesium_Graphics_Config_Json>("/map/graphics", to_server_config(config));
const normalized = from_server_config(response.data);
emit_cesium_graphics_config(normalized);
return normalized;
}
export function emit_cesium_graphics_config(config: Cesium_Graphics_Config) {
const normalized = normalize_cesium_graphics_config(config);
window.dispatchEvent(new CustomEvent<Cesium_Graphics_Config>(cesium_graphics_config_event, {detail: normalized}));
}
export class Cesium_Render_Settings extends enhance.Base {
config: Cesium_Graphics_Config = {...default_cesium_graphics_config};
number_input_drafts: Partial<Record<Number_Config_Key, number | null>> = {};
async on_mount() {
this.config = await load_cesium_graphics_config();
this.number_input_drafts = {};
this.flush();
}
number_input_value(key: Number_Config_Key): number | null {
return Object.prototype.hasOwnProperty.call(this.number_input_drafts, key) ? this.number_input_drafts[key]! : this.config[key];
}
set_number(key: Number_Config_Key, value: number | null) {
this.number_input_drafts[key] = value;
if (value === null) {
this.flush();
return;
}
this.config = normalize_cesium_graphics_config({...this.config, [key]: value});
emit_cesium_graphics_config(this.config);
this.flush();
}
blur_number(key: Number_Config_Key) {
if (Object.prototype.hasOwnProperty.call(this.number_input_drafts, key)) {
delete this.number_input_drafts[key];
this.flush();
}
}
set_boolean(key: Boolean_Config_Key, value: boolean) {
this.config = normalize_cesium_graphics_config({...this.config, [key]: value});
emit_cesium_graphics_config(this.config);
this.flush();
}
reset() {
this.config = {...default_cesium_graphics_config};
this.number_input_drafts = {};
emit_cesium_graphics_config(this.config);
this.flush();
}
apply_preset(key: keyof typeof graphics_presets) {
this.config = {...graphics_presets[key]};
this.number_input_drafts = {};
emit_cesium_graphics_config(this.config);
this.flush();
}
async save() {
this.config = await save_cesium_graphics_config(this.config);
this.number_input_drafts = {};
message.success("Cesium画质设置已保存");
this.flush();
}
render_label(label: string, key: keyof Cesium_Graphics_Config) {
return (
<Space size={4}>
<span>{label}</span>
<Tooltip placement="right" title={<div style={{maxWidth: 360}}>{graphics_help[key].map((line) => <div key={line}>{line}</div>)}</div>}>
<QuestionCircleOutlined style={{color: "#1677ff"}} />
</Tooltip>
</Space>
);
}
render_switch(label: string, key: Boolean_Config_Key) {
return (
<Space style={{display: "flex", justifyContent: "space-between", width: 280}}>
{this.render_label(label, key)}
<Switch checked={this.config[key]} onChange={(checked) => this.set_boolean(key, checked)} />
</Space>
);
}
render_number(label: string, key: Number_Config_Key, min: number, max: number, step: number) {
return (
<InputNumber
addonBefore={this.render_label(label, key)}
min={min}
max={max}
step={step}
value={this.number_input_value(key)}
style={{width: 280}}
onChange={(value) => this.set_number(key, value)}
onBlur={() => this.blur_number(key)}
/>
);
}
render_terrain_exaggeration() {
const disabled = !this.config.terrainEnabledIn3D;
return (
<Space direction="vertical" size={4} style={{width: 280}}>
{this.render_label("地形起伏倍率", "terrainExaggeration")}
<InputNumber
step={0.1}
disabled={disabled}
value={this.number_input_value("terrainExaggeration")}
style={{width: 280}}
onChange={(value) => this.set_number("terrainExaggeration", value)}
onBlur={() => this.blur_number("terrainExaggeration")}
/>
</Space>
);
}
render(props: any) {
return (
<div style={setting_style}>
<h2 style={row_style}>Cesium画质设置</h2>
<Space direction="vertical" size={8} style={col_style}>
<Space>
<Button onClick={() => this.apply_preset("low")}></Button>
<Button onClick={() => this.apply_preset("medium")}></Button>
<Button onClick={() => this.apply_preset("high")}></Button>
</Space>
{this.render_switch("显示FPS", "debugShowFramesPerSecond")}
{this.render_number("目标帧率", "targetFrameRate", 1, 120, 1)}
{this.render_switch("浏览器推荐分辨率", "useBrowserRecommendedResolution")}
{this.render_number("分辨率缩放", "resolutionScale", 0.2, 1.5, 0.1)}
{this.render_number("MSAA采样", "msaaSamples", 1, 8, 1)}
{this.render_switch("FXAA", "fxaa")}
{this.render_switch("阴影", "shadows")}
{this.render_switch("地球光照", "enableLighting")}
{this.render_switch("真实日照", "solarLighting")}
{this.render_number("太阳光强度", "solarLightIntensity", 0, 10, 0.1)}
{this.render_number("地球细节误差", "maximumScreenSpaceError", 1, 16, 1)}
{this.render_switch("3D启用地形", "terrainEnabledIn3D")}
{this.render_terrain_exaggeration()}
{this.render_switch("限制地形层级", "terrainLimitMaximumLevel")}
{this.render_number("地形最大层级", "terrainMaximumLevel", 0, 24, 1)}
{this.render_number("地形缓存瓦片数", "terrainCacheTiles", 16, 2048, 16)}
<Space>
<Button onClick={() => this.reset()}></Button>
<Button type="primary" onClick={() => this.save()}></Button>
</Space>
</Space>
</div>
);
}
}