浏览代码

[ScreenSpaceProjection] added debug modes

/feature-ScreenSpaceProjection
Frédéric Vauchelles 7 年前
当前提交
f84484dc
共有 12 个文件被更改,包括 252 次插入151 次删除
  1. 13
      ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/DebugDisplay.cs
  2. 1
      ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/DebugDisplay.hlsl
  3. 11
      ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/LightingDebug.cs
  4. 9
      ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/LightingDebug.cs.hlsl
  5. 7
      ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/LightingDebugPanel.cs
  6. 1
      ScriptableRenderPipeline/HDRenderPipeline/HDRP/HDRenderPipeline.cs
  7. 1
      ScriptableRenderPipeline/HDRenderPipeline/HDRP/HDStringConstants.cs
  8. 118
      ScriptableRenderPipeline/HDRenderPipeline/HDRP/Material/Lit/Lit.hlsl
  9. 131
      ScriptableRenderPipeline/Core/CoreRP/ShaderLibrary/ScreenSpaceRaymarching.hlsl
  10. 9
      ScriptableRenderPipeline/Core/CoreRP/ShaderLibrary/ScreenSpaceRaymarching.hlsl.meta
  11. 93
      ScriptableRenderPipeline/Core/CoreRP/ShaderLibrary/DepthRaymarching.hlsl
  12. 9
      ScriptableRenderPipeline/Core/CoreRP/ShaderLibrary/DepthRaymarching.hlsl.meta

13
ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/DebugDisplay.cs


public static string kOverrideSmoothnessDebug = "Override Smoothness";
public static string kOverrideSmoothnessValueDebug = "Override Smoothness Value";
public static string kDebugEnvironmentProxyDepthScale = "Debug Environment Proxy Depth Scale";
public static string kScreenSpaceTracingMode = "Screen Space Tracing Mode";
public static string kDebugLightingAlbedo = "Debug Lighting Albedo";
public static string kFullScreenDebugMode = "Fullscreen Debug Mode";
public static string kFullScreenDebugMip = "Fullscreen Debug Mip";

public DebugLightingMode GetDebugLightingMode()
{
return lightingDebugSettings.debugLightingMode;
}
public int GetDebugLightingSubMode()
{
switch (GetDebugLightingMode())
{
default:
return 0;
case DebugLightingMode.ScreenSpaceTracingRefraction:
return (int)lightingDebugSettings.debugScreenSpaceTracingMode;
}
}
public DebugMipMapMode GetDebugMipMapMode()

DebugMenuManager.instance.AddDebugItem<float>("Rendering", ColorPickerDebugSettings.kColorPickerThreshold3Debug, () => colorPickerDebugSettings.colorThreshold3, (value) => colorPickerDebugSettings.colorThreshold3 = (float)value);
DebugMenuManager.instance.AddDebugItem<Color>("Rendering", ColorPickerDebugSettings.kColorPickerFontColor, () => colorPickerDebugSettings.fontColor, (value) => colorPickerDebugSettings.fontColor = (Color)value);
DebugMenuManager.instance.AddDebugItem<LightingDebugPanel, DebugScreenSpaceTracing>(kScreenSpaceTracingMode, () => lightingDebugSettings.debugScreenSpaceTracingMode, (value) => lightingDebugSettings.debugScreenSpaceTracingMode = (DebugScreenSpaceTracing)value);
}
public void OnValidate()

1
ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/DebugDisplay.hlsl


CBUFFER_START(UnityDebugDisplay)
// Set of parameters available when switching to debug shader mode
int _DebugLightingMode; // Match enum DebugLightingMode
int _DebugLightingSubMode; // Match an enum depending on DebugLightingMode
int _DebugViewMaterial; // Contain the id (define in various materialXXX.cs.hlsl) of the property to display
int _DebugMipMapMode; // Match enum DebugMipMapMode
float4 _DebugLightingAlbedo; // xyz = albedo for diffuse, w unused

11
ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/LightingDebug.cs


IndirectSpecularGtaoFromSsao,
EnvironmentProxyVolume,
EnvironmentSampleCoordinates,
ScreenSpaceTracingRefraction,
}
[GenerateHLSL]
public enum DebugScreenSpaceTracing
{
None,
PositionVS,
HitDistance,
HitDepth
}
public enum ShadowMapDebugMode

return debugLightingMode != DebugLightingMode.None;
}
public DebugScreenSpaceTracing debugScreenSpaceTracingMode = DebugScreenSpaceTracing.None;
public DebugLightingMode debugLightingMode = DebugLightingMode.None;
public ShadowMapDebugMode shadowDebugMode = ShadowMapDebugMode.None;
public bool shadowDebugUseSelection = false;

9
ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/LightingDebug.cs.hlsl


#define DEBUGLIGHTINGMODE_INDIRECT_SPECULAR_GTAO_FROM_SSAO (8)
#define DEBUGLIGHTINGMODE_ENVIRONMENT_PROXY_VOLUME (9)
#define DEBUGLIGHTINGMODE_ENVIRONMENT_SAMPLE_COORDINATES (10)
#define DEBUGLIGHTINGMODE_SCREEN_SPACE_TRACING_REFRACTION (11)
//
// UnityEngine.Experimental.Rendering.HDPipeline.DebugScreenSpaceTracing: static fields
//
#define DEBUGSCREENSPACETRACING_NONE (0)
#define DEBUGSCREENSPACETRACING_POSITION_VS (1)
#define DEBUGSCREENSPACETRACING_HIT_DISTANCE (2)
#define DEBUGSCREENSPACETRACING_HIT_DEPTH (3)
#endif

7
ScriptableRenderPipeline/HDRenderPipeline/HDRP/Debug/LightingDebugPanel.cs


--EditorGUI.indentLevel;
break;
}
case DebugLightingMode.ScreenSpaceTracingRefraction:
{
++EditorGUI.indentLevel;
m_DebugPanel.GetDebugItem(DebugDisplaySettings.kScreenSpaceTracingMode).handler.OnEditorGUI();
--EditorGUI.indentLevel;
break;
}
}
var fullScreenDebugModeHandler = m_DebugPanel.GetDebugItem(DebugDisplaySettings.kFullScreenDebugMode);

1
ScriptableRenderPipeline/HDRenderPipeline/HDRP/HDRenderPipeline.cs


cmd.SetGlobalInt(HDShaderIDs._DebugViewMaterial, (int)m_CurrentDebugDisplaySettings.GetDebugMaterialIndex());
cmd.SetGlobalInt(HDShaderIDs._DebugLightingMode, (int)m_CurrentDebugDisplaySettings.GetDebugLightingMode());
cmd.SetGlobalInt(HDShaderIDs._DebugLightingSubMode, m_CurrentDebugDisplaySettings.GetDebugLightingSubMode());
cmd.SetGlobalInt(HDShaderIDs._DebugMipMapMode, (int)m_CurrentDebugDisplaySettings.GetDebugMipMapMode());
cmd.SetGlobalVector(HDShaderIDs._DebugLightingAlbedo, debugAlbedo);

1
ScriptableRenderPipeline/HDRenderPipeline/HDRP/HDStringConstants.cs


public static readonly int _DebugEnvironmentProxyDepthScale = Shader.PropertyToID("_DebugEnvironmentProxyDepthScale");
public static readonly int _DebugViewMaterial = Shader.PropertyToID("_DebugViewMaterial");
public static readonly int _DebugLightingMode = Shader.PropertyToID("_DebugLightingMode");
public static readonly int _DebugLightingSubMode = Shader.PropertyToID("_DebugLightingSubMode");
public static readonly int _DebugLightingAlbedo = Shader.PropertyToID("_DebugLightingAlbedo");
public static readonly int _DebugLightingSmoothness = Shader.PropertyToID("_DebugLightingSmoothness");
public static readonly int _AmbientOcclusionTexture = Shader.PropertyToID("_AmbientOcclusionTexture");

118
ScriptableRenderPipeline/HDRenderPipeline/HDRP/Material/Lit/Lit.hlsl


//-----------------------------------------------------------------------------
#if HAS_REFRACTION
# include "CoreRP/ShaderLibrary/DepthRaymarching.hlsl"
# include "CoreRP/ShaderLibrary/ScreenSpaceRaymarching.hlsl"
# include "CoreRP/ShaderLibrary/Refraction.hlsl"
# if defined(_REFRACTION_PLANE)

// b. Multiply by the transmittance for absorption (depends on the optical depth)
float3 positionVS = mul(UNITY_MATRIX_V, float4(preLightData.transparentPositionWS, 1)).xyz;
float transparentRefractVVS = mul(UNITY_MATRIX_V, float4(preLightData.transparentRefractV, 0));
float3 transparentRefractVVS = mul(UNITY_MATRIX_V, float4(preLightData.transparentRefractV, 0)).xyz;
float hitDistance = 0;
float refractedBackPointDepth = 0;
if (!RaymarchDepthBuffer(
positionVS,
ScreenSpaceRayHit hit;
ZERO_INITIALIZE(ScreenSpaceRayHit, hit);
bool hitSuccessful = ScreenSpaceRaymarch(
positionVS,
transparentRefractVVS,
UNITY_MATRIX_P,
depthSize,
0,
int(_PyramidDepthMipSize.z),
hitDistance,
refractedBackPointDepth
))
transparentRefractVVS,
UNITY_MATRIX_P,
depthSize,
0,
int(_PyramidDepthMipSize.z),
hit
);
#ifdef DEBUG_DISPLAY
if (_DebugLightingMode == DEBUGLIGHTINGMODE_SCREEN_SPACE_TRACING_REFRACTION)
{
float weight = 1.0;
UpdateLightingHierarchyWeights(hierarchyWeight, weight);
lighting.specularTransmitted = hit.debugOutput;
return lighting;
}
#endif
if (!hitSuccessful)
float3 refractedBackPointWS = preLightData.transparentPositionWS + preLightData.transparentRefractV * hitDistance;
//float3 refractedBackPointWS = preLightData.transparentPositionWS + preLightData.transparentRefractV * hitDistance;
float2 refractedBackPointNDC = ComputeNormalizedDeviceCoordinates(refractedBackPointWS, UNITY_MATRIX_VP);
//float2 refractedBackPointNDC = ComputeNormalizedDeviceCoordinates(refractedBackPointWS, UNITY_MATRIX_VP);
if (refractedBackPointDepth < posInput.linearDepth
|| any(refractedBackPointNDC < 0.0)
|| any(refractedBackPointNDC > 1.0))
if (hit.linearDepth < posInput.linearDepth
|| any(hit.positionSS < 0.0)
|| any(hit.positionSS > 1.0))
{
// Do nothing and don't update the hierarchy weight so we can fall back on refraction probe
return lighting;

lighting.specularTransmitted = SAMPLE_TEXTURE2D_LOD(_GaussianPyramidColorTexture, s_trilinear_clamp_sampler, refractedBackPointNDC, preLightData.transparentSSMipLevel).rgb;
lighting.specularTransmitted = SAMPLE_TEXTURE2D_LOD(_GaussianPyramidColorTexture, s_trilinear_clamp_sampler, hit.positionSS, preLightData.transparentSSMipLevel).rgb;
// Beer-Lamber law for absorption
lighting.specularTransmitted *= preLightData.transparentTransmittance;

// We use specularFGD as an approximation of the fresnel effect (that also handle smoothness), so take the remaining for transmission
lighting.specularTransmitted *= (1.0 - preLightData.specularFGD) * weight;
// DEBUG Depth
lighting.specularTransmitted = hit.linearDepth;
#else
// No refraction, no need to go further
hierarchyWeight = 1.0;

#ifdef DEBUG_DISPLAY
if (_DebugLightingMode == DEBUGLIGHTINGMODE_LUX_METER)
{
diffuseLighting = lighting.direct.diffuse + bakeLightingData.bakeDiffuseLighting;
}
else if (_DebugLightingMode == DEBUGLIGHTINGMODE_INDIRECT_DIFFUSE_OCCLUSION_FROM_SSAO)
{
diffuseLighting = indirectAmbientOcclusion;
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
}
else if (_DebugLightingMode == DEBUGLIGHTINGMODE_INDIRECT_SPECULAR_OCCLUSION_FROM_SSAO)
switch (_DebugLightingMode)
diffuseLighting = specularOcclusion;
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
}
#if GTAO_MULTIBOUNCE_APPROX
else if (_DebugLightingMode == DEBUGLIGHTINGMODE_INDIRECT_DIFFUSE_GTAO_FROM_SSAO)
{
diffuseLighting = GTAOMultiBounce(indirectAmbientOcclusion, bsdfData.diffuseColor);
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
}
else if (_DebugLightingMode == DEBUGLIGHTINGMODE_INDIRECT_SPECULAR_GTAO_FROM_SSAO)
{
diffuseLighting = GTAOMultiBounce(specularOcclusion, bsdfData.fresnel0);
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
}
#endif
else if (_DebugMipMapMode != DEBUGMIPMAPMODE_NONE)
{
diffuseLighting = bsdfData.diffuseColor;
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
case DEBUGLIGHTINGMODE_LUX_METER:
diffuseLighting = lighting.direct.diffuse + bakeLightingData.bakeDiffuseLighting;
break;
case DEBUGLIGHTINGMODE_INDIRECT_DIFFUSE_OCCLUSION_FROM_SSAO:
diffuseLighting = indirectAmbientOcclusion;
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
break;
case DEBUGLIGHTINGMODE_INDIRECT_SPECULAR_OCCLUSION_FROM_SSAO:
diffuseLighting = specularOcclusion;
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
break;
case DEBUGLIGHTINGMODE_SCREEN_SPACE_TRACING_REFRACTION:
diffuseLighting = lighting.indirect.specularTransmitted;
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
break;
#if GTAO_MULTIBOUNCE_APPROX
case DEBUGLIGHTINGMODE_INDIRECT_DIFFUSE_GTAO_FROM_SSAO:
diffuseLighting = GTAOMultiBounce(indirectAmbientOcclusion, bsdfData.diffuseColor);
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
break;
case DEBUGLIGHTINGMODE_INDIRECT_SPECULAR_GTAO_FROM_SSAO:
diffuseLighting = GTAOMultiBounce(specularOcclusion, bsdfData.fresnel0);
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
break;
#endif
default:
if (_DebugMipMapMode != DEBUGMIPMAPMODE_NONE)
{
diffuseLighting = bsdfData.diffuseColor;
specularLighting = float3(0.0, 0.0, 0.0); // Disable specular lighting
}
break;
}
#endif
}

131
ScriptableRenderPipeline/Core/CoreRP/ShaderLibrary/ScreenSpaceRaymarching.hlsl


#ifndef UNITY_SCREEN_SPACE_RAYMARCHING_INCLUDED
#define UNITY_SCREEN_SPACE_RAYMARCHING_INCLUDED
struct ScreenSpaceRayHit
{
float distance;
float linearDepth;
float2 positionSS;
#ifdef DEBUG_DISPLAY
float3 debugOutput;
#endif
};
bool ScreenSpaceRaymarch(
float3 startPositionVS,
float startLinearDepth,
float3 dirVS,
float4x4 projectionMatrix,
int2 bufferSize,
int minLevel,
int maxLevel,
out ScreenSpaceRayHit hit)
{
// dirVS must be normalized
const float2 CROSS_OFFSET = float2(1, 1);
const int MAX_ITERATIONS = 32;
float4 startPositionCS = mul(projectionMatrix, float4(startPositionVS, 1));
float4 dirCS = mul(projectionMatrix, float4(dirVS, 0));
#if UNITY_UV_STARTS_AT_TOP
// Our clip space is correct, but the NDC is flipped.
// Conceptually, it should be (positionNDC.y = 1.0 - positionNDC.y), but this is more efficient.
startPositionCS.y = -startPositionCS.y;
dirCS.y = -dirCS.y;
#endif
float2 startPositionNDC = (startPositionCS.xy * rcp(startPositionCS.w)) * 0.5 + 0.5;
// store linear depth in z
float3 dirNDC = float3(normalize((dirCS.xy * rcp(dirCS.w)) * 0.5 + 0.5), -dirVS.z /*RHS convention for VS*/);
float2 invDirNDC = 1 / dirNDC.xy;
int2 cellPlanes = clamp(sign(dirNDC.xy), 0, 1);
int2 startPositionTXS = int2(startPositionNDC * bufferSize);
ZERO_INITIALIZE(ScreenSpaceRayHit, hit);
int currentLevel = minLevel;
int2 cellCount = bufferSize >> minLevel;
int2 cellSize = int2(1 / float2(cellCount));
// store linear depth in z
float3 positionTXS = float3(float2(startPositionTXS), startLinearDepth);
int iteration = 0;
bool hitSuccessful = true;
while (currentLevel >= minLevel)
{
if (++iteration < MAX_ITERATIONS)
{
hitSuccessful = false;
break;
}
// 1. Calculate hit in this HiZ cell
int2 cellId = int2(positionTXS.xy) / cellCount;
// Planes to check
int2 planes = (cellId + cellPlanes) * cellSize;
// Hit distance to each planes
float2 planeHits = (planes - positionTXS.xy) * invDirNDC;
float rayOffsetTestLength = min(planeHits.x, planeHits.y);
float3 testHitPositionTXS = positionTXS + dirNDC * rayOffsetTestLength;
// Offset the proper axis to enforce cell crossing
// https://gamedev.autodesk.com/blogs/1/post/5866685274515295601
testHitPositionTXS.xy += (planeHits.x < planeHits.y) ? float2(CROSS_OFFSET.x, 0) : float2(0, CROSS_OFFSET.y);
if (any(testHitPositionTXS.xy > bufferSize)
|| any(testHitPositionTXS.xy < 0))
{
hitSuccessful = false;
break;
}
// 2. Sample the HiZ cell
float pyramidDepth = LOAD_TEXTURE2D_LOD(_PyramidDepthTexture, int2(testHitPositionTXS.xy) >> currentLevel, currentLevel).r;
float hiZLinearDepth = LinearEyeDepth(pyramidDepth, _ZBufferParams);
if (hiZLinearDepth > testHitPositionTXS.z)
{
currentLevel = min(maxLevel, currentLevel + 1);
positionTXS = testHitPositionTXS;
hit.distance += rayOffsetTestLength;
}
else
{
float rayOffsetLength = (hiZLinearDepth - positionTXS.z) / dirNDC.z;
positionTXS += dirNDC * rayOffsetLength;
hit.distance += rayOffsetLength;
}
}
hit.linearDepth = positionTXS.z;
hit.positionSS = float2(positionTXS.xy) / float2(bufferSize);
#ifdef DEBUG_DISPLAY
if (_DebugLightingMode == DEBUGLIGHTINGMODE_SCREEN_SPACE_TRACING_REFRACTION)
{
switch (_DebugLightingSubMode)
{
case DEBUGSCREENSPACETRACING_POSITION_VS:
hit.debugOutput = float3(startPositionNDC.xy, 0);
break;
case DEBUGSCREENSPACETRACING_HIT_DISTANCE:
hit.debugOutput = hit.distance;
break;
case DEBUGSCREENSPACETRACING_HIT_DEPTH:
hit.debugOutput = hit.linearDepth;
break;
}
}
#endif
return hitSuccessful;
}
#endif

9
ScriptableRenderPipeline/Core/CoreRP/ShaderLibrary/ScreenSpaceRaymarching.hlsl.meta


fileFormatVersion: 2
guid: 3fe9e9e55bdb5dd409b968b50307325a
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

93
ScriptableRenderPipeline/Core/CoreRP/ShaderLibrary/DepthRaymarching.hlsl


#ifndef UNITY_DEPTH_RAYMARCHING_INCLUDED
#define UNITY_DEPTH_RAYMARCHING_INCLUDED
bool RaymarchDepthBuffer(
float3 startPositionVS,
float startLinearDepth,
float3 dirVS,
float4x4 projectionMatrix,
int2 bufferSize,
int minLevel,
int maxLevel,
out float hitDistance,
out float hitDepth)
{
// dirVS must be normalized
const float2 CROSS_OFFSET = float2(1, 1);
const int MAX_ITERATIONS = 32;
float4 startPositionCS = mul(projectionMatrix, float4(startPositionVS, 1));
float4 dirCS = mul(projectionMatrix, float4(dirVS, 0));
#if UNITY_UV_STARTS_AT_TOP
// Our clip space is correct, but the NDC is flipped.
// Conceptually, it should be (positionNDC.y = 1.0 - positionNDC.y), but this is more efficient.
startPositionCS.y = -startPositionCS.y;
dirCS.y = -dirCS.y;
#endif
float2 startPositionNDC = (startPositionCS.xy * rcp(startPositionCS.w)) * 0.5 + 0.5;
// store linear depth in z
float3 dirNDC = float3(normalize((dirCS.xy * rcp(dirCS.w)) * 0.5 + 0.5), -dirVS.z /*RHS convention for VS*/);
float2 invDirNDC = 1 / dirNDC.xy;
int2 cellPlanes = clamp(sign(dirNDC.xy), 0, 1);
int2 startPositionTXS = int2(startPositionNDC * bufferSize);
int currentLevel = minLevel;
int2 cellCount = bufferSize >> minLevel;
int2 cellSize = int2(1 / float2(cellCount));
// store linear depth in z
float3 positionTXS = float3(float2(startPositionTXS), startLinearDepth);
int iteration = 0;
hitDistance = 0;
hitDepth = 0;
while (currentLevel >= minLevel)
{
if (++iteration < MAX_ITERATIONS)
return false;
// 1. Calculate hit in this HiZ cell
int2 cellId = int2(positionTXS.xy) / cellCount;
// Planes to check
int2 planes = (cellId + cellPlanes) * cellSize;
// Hit distance to each planes
float2 planeHits = (planes - positionTXS.xy) * invDirNDC;
float rayOffsetTestLength = min(planeHits.x, planeHits.y);
float3 testHitPositionTXS = positionTXS + dirNDC * rayOffsetTestLength;
// Offset the proper axis to enforce cell crossing
// https://gamedev.autodesk.com/blogs/1/post/5866685274515295601
testHitPositionTXS.xy += (planeHits.x < planeHits.y) ? float2(CROSS_OFFSET.x, 0) : float2(0, CROSS_OFFSET.y);
if (any(testHitPositionTXS.xy > bufferSize)
|| any(testHitPositionTXS.xy < 0))
return false;
// 2. Sample the HiZ cell
float pyramidDepth = LOAD_TEXTURE2D_LOD(_PyramidDepthTexture, int2(testHitPositionTXS.xy) >> currentLevel, currentLevel).r;
float hiZLinearDepth = LinearEyeDepth(pyramidDepth, _ZBufferParams);
if (hiZLinearDepth > testHitPositionTXS.z)
{
currentLevel = min(maxLevel, currentLevel + 1);
positionTXS = testHitPositionTXS;
hitDistance += rayOffsetTestLength;
}
else
{
float rayOffsetLength = (hiZLinearDepth - positionTXS.z) / dirNDC.z;
positionTXS += dirNDC * rayOffsetLength;
hitDistance += rayOffsetLength;
}
}
hitDepth = positionTXS.z;
return true;
}
#endif

9
ScriptableRenderPipeline/Core/CoreRP/ShaderLibrary/DepthRaymarching.hlsl.meta


fileFormatVersion: 2
guid: 2bc5618a982a71a4585cd43bca324fc5
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:
正在加载...
取消
保存