using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
///
/// The built-in Atmospheric Rendering pack is expressed through the same
/// public declaration consumed by third-party packs. Renderer implementation
/// code resolves public enum semantics and never recognizes this pack's IDs,
/// so the built-in receives no private capability or lifecycle shortcut.
///
internal static class BuiltInAtmosphericRenderPack
{
internal const string Id = "acdream.atmospheric";
internal static RenderPackDescriptor Descriptor { get; } = new RenderPackDescriptor(
Id,
"Atmospheric Rendering",
new Version(1, 0, 0),
RenderPackApi.Current,
RenderPackTier.Tier2Plus,
[
RenderCapability.MainWorldColorIntermediate,
RenderCapability.FullscreenPasses,
RenderCapability.SceneDepthSampling,
RenderCapability.AuthoredSunDirection,
RenderCapability.AuthoredSunScreenPosition,
RenderCapability.AuthoredWeather,
RenderCapability.DirectionalShadowMaps,
RenderCapability.OutdoorDirectionalShadowCasterReplay,
RenderCapability.AnimatedCasterTransforms,
RenderCapability.AlphaCutoutShadowCasters,
RenderCapability.AuthoredCelestialDirectionalLight,
],
[RenderCapability.GpuTimestampQueries],
Resources(),
Passes(),
SceneReplays(),
PipelineVariants(),
QualityPresets(),
Settings(),
AtmospherePolicy())
{
FeatureSummary = "Filmic HDR atmosphere, moving sun-and-moon shadows from terrain, "
+ "trees, buildings, players, and monsters, plus optional volumetric shafts.",
};
internal static IRenderPackAssets CreateAssets(string shaderDirectory) =>
new DirectoryRenderPackAssets(shaderDirectory);
private static IReadOnlyList Resources() =>
[
Image("world-hdr", RenderResourceSemantic.MainWorldHdr,
RenderFormatClass.HdrColor, 1.0, 1.0, 32L * 1024 * 1024),
Image("bloom-a", RenderResourceSemantic.BloomPing,
RenderFormatClass.HdrColor, 0.5, 0.5, 8L * 1024 * 1024),
Image("bloom-b", RenderResourceSemantic.BloomPong,
RenderFormatClass.HdrColor, 0.5, 0.5, 8L * 1024 * 1024),
Image("sun-mask", RenderResourceSemantic.SunOcclusionMask,
RenderFormatClass.SingleChannel, 0.25, 0.25, 2L * 1024 * 1024),
Image("sun-rays", RenderResourceSemantic.SunRays,
RenderFormatClass.HdrColor, 0.25, 0.25, 2L * 1024 * 1024),
new RenderResourceDeclaration(
"directional-shadow-depth",
RenderResourceKind.Image2DArray,
RenderFormatClass.DirectionalDepth,
new RenderExtentDeclaration(RenderExtentMode.AbsolutePixels, 1024, 1024, Layers: 2),
SizeBytes: 0,
RenderResourceUsage.Sampled | RenderResourceUsage.DepthAttachment,
RenderResourceLifetime.ActivePack,
EstimatedResidentBytes: 8L * 1024 * 1024)
with { Semantic = RenderResourceSemantic.DirectionalShadowDepth },
Image("volumetric", RenderResourceSemantic.VolumetricShafts,
RenderFormatClass.HdrColor, 0.25, 0.25, 2L * 1024 * 1024),
];
private static IReadOnlyList Passes() =>
[
Pass(
"directional-shadow-depth",
RenderPassSemantic.DirectionalShadowDepth,
RenderPassHook.ShadowDepthBeforeWorld,
"directional_shadow_world_opaque.vert.spv",
"directional_shadow_world_opaque.frag.spv",
[RenderSemanticInput.CameraMatrices,
RenderSemanticInput.SelectedCelestialDirectionalLight,
RenderSemanticInput.ShadowCasterTransforms, RenderSemanticInput.ActiveDayGroup,
RenderSemanticInput.Weather],
[],
["directional-shadow-depth"]),
Pass(
"sun-occlusion",
RenderPassSemantic.SunOcclusion,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_sun_occlusion.vert.spv",
"atmospheric_sun_occlusion.frag.spv",
[RenderSemanticInput.SceneDepth, RenderSemanticInput.SunScreenPosition,
RenderSemanticInput.ActiveDayGroup, RenderSemanticInput.Weather],
[],
["sun-mask"]),
Pass(
"sun-rays",
RenderPassSemantic.SunRays,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_sun_rays.vert.spv",
"atmospheric_sun_rays.frag.spv",
[RenderSemanticInput.SunScreenPosition, RenderSemanticInput.FrameTime],
["sun-mask"],
["sun-rays"]),
Pass(
"volumetric-shafts",
RenderPassSemantic.VolumetricShafts,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_volumetric.vert.spv",
"atmospheric_volumetric.frag.spv",
[RenderSemanticInput.SceneDepth, RenderSemanticInput.CameraMatrices,
RenderSemanticInput.SunDirection, RenderSemanticInput.DirectionalShadowMaps,
RenderSemanticInput.ActiveDayGroup, RenderSemanticInput.Weather],
["directional-shadow-depth"],
["volumetric"]),
Pass(
"bloom-downsample",
RenderPassSemantic.BloomDownsample,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_bloom_downsample.vert.spv",
"atmospheric_bloom_downsample.frag.spv",
[RenderSemanticInput.WorldColor],
["sun-rays", "volumetric"],
["bloom-a"]),
Pass(
"bloom-blur-horizontal",
RenderPassSemantic.BloomBlurHorizontal,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_bloom_blur.vert.spv",
"atmospheric_bloom_blur.frag.spv",
[RenderSemanticInput.FrameTime],
["bloom-a"],
["bloom-b"]),
Pass(
"bloom-blur-vertical",
RenderPassSemantic.BloomBlurVertical,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_bloom_blur.vert.spv",
"atmospheric_bloom_blur.frag.spv",
[RenderSemanticInput.FrameTime],
["bloom-b"],
["bloom-a"]),
Pass(
"filmic-composite",
RenderPassSemantic.FilmicComposite,
RenderPassHook.ToneMap,
"atmospheric_filmic.vert.spv",
"atmospheric_filmic.frag.spv",
[RenderSemanticInput.WorldColor, RenderSemanticInput.FrameTime],
["bloom-a", "sun-rays", "volumetric"],
[]),
];
private static IReadOnlyList SceneReplays() =>
[
new SceneReplayDeclaration(
"outdoor-directional-shadow-casters",
RenderSceneReplaySemantic.OutdoorDirectionalShadowCasters,
RenderCasterClass.Terrain
| RenderCasterClass.OpaqueWorld
| RenderCasterClass.AlphaCutoutWorld
| RenderCasterClass.AnimatedOpaque
| RenderCasterClass.AnimatedAlphaCutout,
ViewCount: 4),
];
private static IReadOnlyList PipelineVariants() =>
[
Variant("terrain-shadow-caster", RenderPipelineVariantSemantic.TerrainDirectionalShadowCaster,
RenderPipelineBaseSemantic.Terrain,
"directional_shadow_terrain.vert.spv", "directional_shadow_terrain.frag.spv",
RenderMaterialClass.Opaque,
[RenderSemanticInput.CameraMatrices]),
Variant("world-shadow-opaque", RenderPipelineVariantSemantic.WorldOpaqueDirectionalShadowCaster,
RenderPipelineBaseSemantic.WorldMesh,
"directional_shadow_world_opaque.vert.spv", "directional_shadow_world_opaque.frag.spv",
RenderMaterialClass.Opaque | RenderMaterialClass.AnimatedOpaque,
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
Variant("world-shadow-cutout", RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster,
RenderPipelineBaseSemantic.WorldMesh,
"directional_shadow_world_cutout.vert.spv", "directional_shadow_world_cutout.frag.spv",
RenderMaterialClass.AlphaCutout | RenderMaterialClass.AnimatedAlphaCutout,
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
Variant("terrain-shadow-caster-multiview", RenderPipelineVariantSemantic.TerrainMultiviewDirectionalShadowCaster,
RenderPipelineBaseSemantic.Terrain,
"directional_shadow_terrain_multiview.vert.spv", "directional_shadow_terrain_multiview.frag.spv",
RenderMaterialClass.Opaque,
[RenderSemanticInput.CameraMatrices]),
Variant("world-shadow-opaque-multiview", RenderPipelineVariantSemantic.WorldOpaqueMultiviewDirectionalShadowCaster,
RenderPipelineBaseSemantic.WorldMesh,
"directional_shadow_world_opaque_multiview.vert.spv", "directional_shadow_world_opaque_multiview.frag.spv",
RenderMaterialClass.Opaque | RenderMaterialClass.AnimatedOpaque,
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
Variant("world-shadow-cutout-multiview", RenderPipelineVariantSemantic.WorldAlphaCutoutMultiviewDirectionalShadowCaster,
RenderPipelineBaseSemantic.WorldMesh,
"directional_shadow_world_cutout_multiview.vert.spv", "directional_shadow_world_cutout_multiview.frag.spv",
RenderMaterialClass.AlphaCutout | RenderMaterialClass.AnimatedAlphaCutout,
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
Variant("terrain-shadow-receiver", RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver,
RenderPipelineBaseSemantic.Terrain,
"terrain_atmospheric.vert.spv", "terrain_atmospheric.frag.spv",
RenderMaterialClass.Opaque,
[RenderSemanticInput.DirectionalShadowMaps,
RenderSemanticInput.SelectedCelestialDirectionalLight]),
Variant("world-shadow-receiver", RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver,
RenderPipelineBaseSemantic.WorldMesh,
"mesh_atmospheric.vert.spv", "mesh_atmospheric.frag.spv",
RenderMaterialClass.Opaque | RenderMaterialClass.AlphaCutout
| RenderMaterialClass.AnimatedOpaque | RenderMaterialClass.AnimatedAlphaCutout,
[RenderSemanticInput.DirectionalShadowMaps,
RenderSemanticInput.SelectedCelestialDirectionalLight]),
];
private static IReadOnlyList QualityPresets() =>
[
Preset("low", "Low", RenderQualitySemantic.Low,
64, 2.0, 3.0, 0.15, 0.50, 768, 2, 72, 0.25) with
{
ExecutionHints =
RenderQualityExecutionHints.MultiviewDirectionalShadowCascades,
},
Preset("medium", "Medium", RenderQualitySemantic.Medium,
128, 3.25, 4.50, 0.25, 0.75, 1536, 3, 144, 0.5),
Preset("high", "High", RenderQualitySemantic.High,
256, 4.50, 6.00, 0.35, 1.00, 2048, 4, 240, 0.5),
Preset("auto", "Auto", RenderQualitySemantic.Automatic,
128, 3.25, 4.50, 0.25, 0.75, 1536, 3, 144, 0.5)
with
{
SettingOverrides =
[
new RenderQualitySettingOverride("automatic-quality", "true"),
new RenderQualitySettingOverride("volumetric-strength", "0.35"),
new RenderQualitySettingOverride("volumetric-ray-steps", "40"),
new RenderQualitySettingOverride("sun-shadow-strength", "0.72"),
new RenderQualitySettingOverride("sun-shadow-reach-metres", "144"),
new RenderQualitySettingOverride("sun-shadow-pcf-taps", "9"),
new RenderQualitySettingOverride("sun-ray-strength", "0.55"),
],
AutoEligible = false,
},
];
private static IReadOnlyList Settings() =>
[
Float("bloom-strength", "Bloom strength", RenderSettingSemantic.BloomStrength,
0.65, 0, 2, 0.05),
Float("filmic-strength", "Filmic tonemap strength", RenderSettingSemantic.FilmicStrength,
1.0, 0, 1, 0.05),
// Campaign VM VM3: exposure 0.80 is unchanged by the linear-light post
// stack move. Before VM3, exposure multiplied gamma-encoded values
// directly; after VM3 it multiplies decoded linear light instead, but
// the accepted midtone survives the swap almost exactly:
// encode(acesFitted(0.80 * decode(0.46))) ~= 0.50, matching the old
// gamma-space pipeline's 0.51 for the same 0.46 mid-grey input (see
// AtmosphericColorPipelineTests for the pinned numbers). Two visible
// side effects the owner's visual gate judges: highlights now retain
// more (a gamma 0.9 input moves from ~0.74 to ~0.85 through the full
// exposure/ACES pipeline) and blacks deepen slightly (gamma 0.1 moves
// from ~0.09 to ~0.05).
Float("exposure", "Exposure", RenderSettingSemantic.Exposure,
0.80, 0.25, 4, 0.05),
Float("grade-saturation", "Colour saturation", RenderSettingSemantic.GradeSaturation,
1.0, 0, 2, 0.05),
Float("grade-contrast", "Colour contrast", RenderSettingSemantic.GradeContrast,
1.0, 0.5, 2, 0.05),
// Campaign VM VM3: re-derived for the linear-light post stack. The
// vignette multiply now happens on linear colour before the final
// encode, so a corner factor of (1 - strength) displays as
// (1 - strength)^(1/2.2), not (1 - strength). The accepted look was
// strength 0.12 under the OLD gamma-space pipeline: a corner
// multiplied directly by 0.88, i.e. a 12% on-screen darkening. Under
// strength 0.12 in linear that same 0.88 would display as
// 0.88^(1/2.2) ~= 0.9435 — only a 5.6% darkening, visibly weaker.
// Solving (1 - strength)^(1/2.2) = 0.88 for strength gives
// strength = 1 - 0.88^2.2 ~= 0.245, which reproduces the accepted
// 12% corner darkening on screen (see
// AtmosphericColorPipelineTests for the pinned encode(1-0.245)~=0.88
// check). Step moves 0.01 -> 0.005 so the exact derived default
// lands on the settings grid (RenderPackSettingValueCodec requires
// every declared default to be step-aligned from the minimum;
// 0.245 / 0.01 is not integral, 0.245 / 0.005 = 49 is) — a finer
// slider, not a coarser one.
Float("vignette-strength", "Vignette strength", RenderSettingSemantic.VignetteStrength,
0.245, 0, 1, 0.005),
Float("sun-ray-strength", "Sun-ray strength", RenderSettingSemantic.SunRayStrength,
0.55, 0, 2, 0.05),
Float("sun-shadow-strength", "Directional-shadow strength",
RenderSettingSemantic.DirectionalShadowStrength, 0.72, 0, 1, 0.02),
Integer("sun-shadow-reach-metres", "Directional-shadow reach (metres)",
RenderSettingSemantic.DirectionalShadowReachMetres, 240, 16, 240, 1),
Choice("sun-shadow-pcf-taps", "Directional-shadow filter taps",
RenderSettingSemantic.DirectionalShadowPcfTaps, "9", ["1", "9", "25"]),
Float("volumetric-strength", "Volumetric-shaft strength",
RenderSettingSemantic.VolumetricStrength, 0.35, 0, 1, 0.01),
Integer("volumetric-ray-steps", "Volumetric ray-march steps",
RenderSettingSemantic.VolumetricRayMarchSteps, 40, 8, 64, 8),
new RenderSettingDeclaration(
"automatic-quality",
"Automatic quality",
RenderSettingKind.Boolean,
"false",
null,
null,
null,
[])
with { Semantic = RenderSettingSemantic.AutomaticQuality },
// Campaign VM VM6: foliage wind. wind-strength is a global multiplier
// over the per-day-group mean/gust table (AtmospherePolicy below);
// wind-direction-degrees has no authored retail wind direction to
// read (register row: "no authored wind direction" — foliage wind is
// itself a render-only approximation), so it is a plain pack default.
new RenderSettingDeclaration(
"wind-enabled",
"Foliage wind",
RenderSettingKind.Boolean,
"true",
null,
null,
null,
[])
with { Semantic = RenderSettingSemantic.WindEnabled },
Float("wind-strength", "Foliage wind strength", RenderSettingSemantic.WindStrength,
1.0, 0, 2, 0.05),
Float("wind-direction-degrees", "Foliage wind direction (degrees)",
RenderSettingSemantic.WindDirectionDegrees, 225, 0, 360, 5),
Float("wind-lean-metres", "Foliage lean amplitude (metres)",
RenderSettingSemantic.WindLeanMetres, 0.25, 0, 1, 0.01),
Float("wind-branch-metres", "Foliage branch-swing amplitude (metres)",
RenderSettingSemantic.WindBranchMetres, 0.15, 0, 1, 0.01),
Float("wind-flutter-metres", "Foliage flutter amplitude (metres)",
RenderSettingSemantic.WindFlutterMetres, 0.05, 0, 0.5, 0.005),
Float("wind-canopy-height-metres", "Foliage canopy height (metres)",
RenderSettingSemantic.WindCanopyHeightMetres, 8, 2, 30, 0.5),
];
private static AtmospherePolicyDeclaration AtmospherePolicy() => new(
[
new SunElevationResponsePoint(-90, 0),
new SunElevationResponsePoint(-3, 0),
new SunElevationResponsePoint(4, 1),
new SunElevationResponsePoint(22, 0.75),
new SunElevationResponsePoint(55, 0),
new SunElevationResponsePoint(90, 0),
],
[
new ActiveDayGroupMultiplier(0, 1.0),
new ActiveDayGroupMultiplier(1, 0.35),
new ActiveDayGroupMultiplier(2, 0.20),
])
{
DirectionalShadowLightElevationResponse =
[
new SunElevationResponsePoint(-90, 0),
new SunElevationResponsePoint(1, 0),
new SunElevationResponsePoint(12, 1),
new SunElevationResponsePoint(90, 1),
],
VolumetricShaftSunElevationResponse =
[
new SunElevationResponsePoint(-90, 0),
new SunElevationResponsePoint(0, 0),
new SunElevationResponsePoint(6, 1),
new SunElevationResponsePoint(18, 1),
new SunElevationResponsePoint(70, 0),
new SunElevationResponsePoint(90, 0),
],
// Campaign VM VM6, corrected in the fix round: keyed by the
// DAT-classified AcDream.Core.World.WeatherKind, not the raw
// activeDayGroup index — WeatherState.cs's existing name
// classification (e.g. "Sunny" -> Clear, a name containing
// "fog"/"cloud"/"overcast"/"dark"/"storm"/"snow"/"rain" -> the
// matching kind) already turns a day group's authored DAT name
// into one of these five real weather kinds; that fact was
// already threaded through AtmosphericFrameInputs.Weather /
// uAtmosphereWeather.x, so this table reuses it instead of
// guessing a meaning for the raw index. Every WeatherKind member
// is declared; nothing falls through to the Clear fallback in
// production.
FoliageWindByWeather =
[
new FoliageWindWeatherPoint("Clear", 0.25, 0.15),
new FoliageWindWeatherPoint("Overcast", 0.60, 0.35),
new FoliageWindWeatherPoint("Rain", 0.85, 0.60),
new FoliageWindWeatherPoint("Snow", 0.35, 0.20),
new FoliageWindWeatherPoint("Storm", 1.00, 0.75),
],
};
private static RenderResourceDeclaration Image(
string id,
RenderResourceSemantic semantic,
RenderFormatClass format,
double widthScale,
double heightScale,
long estimatedBytes) => new RenderResourceDeclaration(
id,
RenderResourceKind.Image2D,
format,
new RenderExtentDeclaration(
RenderExtentMode.RelativeToMainWorld,
widthScale,
heightScale),
SizeBytes: 0,
RenderResourceUsage.Sampled | RenderResourceUsage.ColorAttachment,
RenderResourceLifetime.ActivePack,
estimatedBytes)
{ Semantic = semantic };
private static RenderPassDeclaration Pass(
string id,
RenderPassSemantic semantic,
RenderPassHook hook,
string vertex,
string fragment,
IReadOnlyList semantics,
IReadOnlyList reads,
IReadOnlyList writes) =>
new(id, hook, vertex, fragment, semantics, reads, writes)
{
Semantic = semantic,
};
private static PipelineVariantDeclaration Variant(
string id,
RenderPipelineVariantSemantic variantSemantic,
RenderPipelineBaseSemantic semantic,
string vertex,
string fragment,
RenderMaterialClass materials,
IReadOnlyList inputs) =>
new(id, semantic, vertex, fragment, materials, inputs)
{
Semantic = variantSemantic,
};
private static RenderQualityPreset Preset(
string id,
string displayName,
RenderQualitySemantic semantic,
long maxMiB,
double gpuP50,
double gpuP99,
double cpuP50,
double cpuP99,
int shadowResolution,
int cascades,
int shadowReachMetres,
double postScale) => new RenderQualityPreset(
id,
displayName,
semantic == RenderQualitySemantic.Low
? [RenderCapability.DirectionalShadowMaps,
RenderCapability.MultiviewDirectionalShadowCascades]
: [RenderCapability.DirectionalShadowMaps],
[
Override("directional-shadow-depth", shadowResolution, shadowResolution, cascades,
4L * shadowResolution * shadowResolution * cascades),
RelativeOverride("bloom-a", postScale),
RelativeOverride("bloom-b", postScale),
RelativeOverride("sun-mask", id == "low" ? 0.25 : 0.5),
RelativeOverride("sun-rays", id == "low" ? 0.25 : 0.5),
RelativeOverride("volumetric", id == "high" ? 0.5 : 0.25),
],
[
new RenderQualitySettingOverride("automatic-quality", "false"),
new RenderQualitySettingOverride("volumetric-strength", id == "low" ? "0" : "0.35"),
new RenderQualitySettingOverride(
"volumetric-ray-steps",
semantic switch
{
RenderQualitySemantic.Low => "24",
RenderQualitySemantic.High => "56",
_ => "40",
}),
new RenderQualitySettingOverride("sun-shadow-strength", "0.72"),
new RenderQualitySettingOverride("sun-shadow-reach-metres", shadowReachMetres.ToString()),
new RenderQualitySettingOverride(
"sun-shadow-pcf-taps",
semantic switch
{
RenderQualitySemantic.Low => "1",
RenderQualitySemantic.High => "25",
_ => "9",
}),
// The renderer recognizes this bounded preset fact; it remains
// visible here instead of becoming a hidden cascade constant.
new RenderQualitySettingOverride("sun-ray-strength", id == "low" ? "0.4" : "0.55"),
// Campaign VM VM6: Low is lean + branch only, no flutter — the
// per-vertex hash term is the cheapest of the three to drop
// and the least visible at Low's other reduced settings.
new RenderQualitySettingOverride("wind-flutter-metres", id == "low" ? "0" : "0.05"),
],
maxMiB * 1024 * 1024,
gpuP50,
gpuP99,
cpuP50,
cpuP99)
{ Semantic = semantic };
private static RenderQualityResourceOverride Override(
string id,
int width,
int height,
int layers,
long bytes) => new(
id,
new RenderExtentDeclaration(RenderExtentMode.AbsolutePixels, width, height, layers),
SizeBytes: 0,
EstimatedResidentBytes: bytes);
private static RenderQualityResourceOverride RelativeOverride(string id, double scale) =>
new(
id,
new RenderExtentDeclaration(RenderExtentMode.RelativeToMainWorld, scale, scale),
SizeBytes: 0,
EstimatedResidentBytes: 0);
private static RenderSettingDeclaration Float(
string id,
string displayName,
RenderSettingSemantic semantic,
double defaultValue,
double min,
double max,
double step) => new RenderSettingDeclaration(
id,
displayName,
RenderSettingKind.Float,
defaultValue.ToString(System.Globalization.CultureInfo.InvariantCulture),
min,
max,
step,
[])
{ Semantic = semantic };
private static RenderSettingDeclaration Integer(
string id,
string displayName,
RenderSettingSemantic semantic,
int defaultValue,
int min,
int max,
int step) => new RenderSettingDeclaration(
id,
displayName,
RenderSettingKind.Integer,
defaultValue.ToString(System.Globalization.CultureInfo.InvariantCulture),
min,
max,
step,
[])
{ Semantic = semantic };
private static RenderSettingDeclaration Choice(
string id,
string displayName,
RenderSettingSemantic semantic,
string defaultValue,
IReadOnlyList choices) => new RenderSettingDeclaration(
id,
displayName,
RenderSettingKind.Choice,
defaultValue,
null,
null,
null,
choices)
{ Semantic = semantic };
}
internal sealed class DirectoryRenderPackAssets : IRenderPackAssets
{
private readonly string _root;
internal DirectoryRenderPackAssets(string root)
{
ArgumentException.ThrowIfNullOrWhiteSpace(root);
_root = Path.GetFullPath(root);
}
public Stream OpenRead(string assetKey)
{
ArgumentException.ThrowIfNullOrWhiteSpace(assetKey);
string normalized = assetKey.Replace('/', Path.DirectorySeparatorChar);
string path = Path.GetFullPath(Path.Combine(_root, normalized));
string relative = Path.GetRelativePath(_root, path);
if (Path.IsPathRooted(relative)
|| relative == ".."
|| relative.StartsWith($"..{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
throw new UnauthorizedAccessException("The asset key escapes the render-pack root.");
return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
}
}