3.12 阴影映射算法的解释

原文地址:http://www.riemers.net/eng/Tutorials/XNA/Csharp/Series3/Shadow_map.php

现在我们知道了HLSL的基础知识并定义了第一个光源,现在可以创建更复杂的东西了。现在你已经在场景中添加了一个光源,你应该注意到汽车和路灯并没有在墙上或人行道上投下阴影。

让我们看一下如何在场景中添加真实的阴影。我们想让阴影算法完全是动态的:而其只需定义光源的位置和方向一次,在光照范围内的所有物体都会自动产生阴影。

让我们首先屏幕左下角的汽车开始。它的头灯照向屏幕的右方,所以灯光与另一辆车和路灯柱相遇,它们应该在墙上投下影子。问题是:我们如何知道墙上的哪些像素被照亮,哪些会有阴影?

在解释深度映射算法时,你可以看一下下面的图片,图中画出了2个主要步骤。第一步绘制从汽车头灯看起来的场景。意思是我们将相机移动到汽车头灯的位置,使用这个点的视场,我们唯一感兴趣的东西是每个像素离开头灯的距离,例如,第一个灯柱离开头灯有4米远。非常重要的是:在第一个灯柱之后的墙上的像素不会被头灯看到。将墙上的这些像素的离开光源的距离设为4米。我们将这个距离信息存储在阴影贴图(shadow map)或深度贴图(depth map)中。

第二步,使用常规方式绘制场景。但这次,对每个像素,我们首先计算它离开头灯的距离,并将这个距离与存储在深度贴图中的深度值做比较。对大多数对象,这两个距离是相同的。例如路灯,离开头灯的距离还是4米。

但是,当计算灯后面的墙的像素时,我们发现这个距离是5米,它与存储在像素中的4米不同。通过这种方式,我们知道这些像素无法被头灯看到,不应该被照亮。

阴影示意图

因此我们的第一步是:绘制深度贴图。我们将定义一个新的technique:ShadowMap,它将场景深度绘制到屏幕中。你已经在HLSL文件的最后添加了technique定义:

technique ShadowMap
{
    pass Pass0
    {
        VertexShader = compile vs_2_0 ShadowMapVertexShader(); 
        PixelShader = compile ps_2_0 ShadowMapPixelShader(); 
    } 
}

顶点着色器很简单,它只需生成一个output,在technique定义前添加以下代码:

struct SMapVertexToPixel 
{
    float4 Position : POSITION; 
    float4 Position2D : TEXCOORD0; 
};

和以往一样,我们需要将2D屏幕坐标提供给插值器和pixel shader。不过这次的屏幕坐标还包含物体到相机的距离,在pixel shader中需要这个信息,因此我们需要使用语义TEXCOORDn中的一个传递这个信息。下面是新的vertex shader:

SMapVertexToPixel ShadowMapVertexShader( float4 inPos : POSITION) 
{
    SMapVertexToPixel Output = (SMapVertexToPixel)0; 
    
    Output.Position = mul(inPos, xLightsWorldViewProjection); 
    Output.Position2D = Output.Position; 
    
    return Output; 
 } 

有一件事非常重要:这里我们使用xLightsWorldViewProjection替代了xWorldViewProjection,因为这次我们需要从汽车头灯看起来的场景而不是从相机中看到的场景。

WorldViewProjection矩阵是三个矩阵的组合(World, View和Projection矩阵)。虽然LightsWorldViewProjection的名称会让你疑惑,但它仍是三个矩阵的组合:同样的World矩阵,但使用光源的View和Projection矩阵,这些View和Projection矩阵和相机的 View和Projection矩阵是不同的。

我们需要在HLSL顶部初始化这个新矩阵:

float4x4 xLightsWorldViewProjection; 

我们会在后面的XNA程序中对这个变量赋值。然后开始编写像素着色器,它仍然只需计算颜色:

struct SMapPixelToFrame 
{
    float4 Color : COLOR0; 
};

下面是像素着色器代码:

SMapPixelToFrame ShadowMapPixelShader(SMapVertexToPixel PSIn) 
{
    SMapPixelToFrame Output = (SMapPixelToFrame)0; 
    Output.Color = PSIn.Position2D.z/PSIn.Position2D.w; 
    
    return Output; 
}

PSIn.Position的X和Y坐标包含了当前像素的屏幕坐标,但Z坐标也非常有用,因为它包含了相机离开像素的距离信息。

但是,这个向量是一个向量和一个4x4矩阵相乘的结果,这个过程发生在顶点着色器中,这个结果向量有四个分量:X,Y,Z和W,我们不能直接使用这四个分量,需要首先将它们除以W分量。W分量叫做齐次分量(homogeneous component)。

将Z分量除以齐次分量之后,结果位于0到1区间,0对应位于近裁平面上的点,1对应远裁平面上的点,这两个平面是在创建Projection矩阵时定义的。以上就是HLSL部分的代码。下面看一下XNA代码,我们需要在XNA代码中传递所有effect变量 。首先从xLightsWorldViewProjection变量开始。因为这个变量基于光源的位置,所有需要在UpdateLightData方法中进行更新,我们需要在代码中添加这个变量:

Matrix lightsViewProjectionMatrix; 

现在我们改变了UpdateLightData方法的内容。我们将光源的位置改变为在车前方,改变了它的光照强度,第三行代码是新的:

private void UpdateLightData() 
{
    ambientPower = 0.2f; 
    
    lightPos = new Vector3(-18, 5, -2); 
    lightPower = 1.0f; 
    Matrix lightsView = Matrix.CreateLookAt(lightPos, new Vector3(-2, 3, -10), new Vector3(0, 1, 0)); 
    Matrix lightsProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver2, 1f, 5f, 100f); 
    lightsViewProjectionMatrix = lightsView* lightsProjection; 
}

为了绘制阴影贴图,我们需要绘制从汽车头灯看起来的场景,我们需要创建一个对应的ViewProjection矩阵。 这和创建相机矩阵的方法几乎是一样的。注意两点:

  1. 投影矩阵的长宽比aspect ratio只需要等于屏幕的的长宽比,本例中我将长宽比设为1f,这样光源会看到一个正方形。
  2. 近裁平面和远裁平面的距离对应距离贴图中的黑色和白色。

我们需要在绘制三角形带和模型前将这个矩阵传递到显卡中,所以在Draw方法中添加以下代码:

effect.Parameters["xLightsWorldViewProjection"].SetValue(Matrix.Identity * lightsViewProjectionMatrix); 

注意ViewProjection矩阵被World矩阵(街道的情况中World矩阵为单位矩阵)相乘以获取WorldViewProjection矩阵。在DrawModel方法中添加以下代码:

currenteffect.Parameters["xLightsWorldViewProjection"].SetValue(worldMatrix * lightsViewProjectionMatrix); 

现在我们需要选择正确的technique绘制场景。因为后面我们需要使用另一个technique 第二次绘制场景,所以现在最好将Draw方法的内容放置在另一个DrawScene方法中:

private void DrawScene(string technique) 
{
    effect.CurrentTechnique = effect.Techniques[technique]; 
    effect.Parameters["xWorldViewProjection"].SetValue(Matrix.Identity * viewMatrix * projectionMatrix); 
    effect.Parameters["xTexture"].SetValue(streetTexture); 
    effect.Parameters["xWorld"].SetValue(Matrix.Identity); 
    effect.Parameters["xLightPos"].SetValue(lightPos); 
    effect.Parameters["xLightPower"].SetValue(lightPower); 
    effect.Parameters["xAmbient"].SetValue(ambientPower); 
    effect.Parameters["xLightsWorldViewProjection"].SetValue(Matrix.Identity * lightsViewProjectionMatrix); 
    
    effect.Begin(); 
    foreach (EffectPass pass in effect.CurrentTechnique.Passes) 
    {
        pass.Begin(); 
        device.VertexDeclaration = vertexDeclaration; device.Vertices[0].SetSource(vertexBuffer, 0, MyOwnVertexFormat.SizeInBytes); 
        device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 18); 
        pass.End(); 
    } 
    effect.End(); 
    
    Matrix car1Matrix = Matrix.CreateScale(4f) * Matrix.CreateRotationY(MathHelper.Pi) * Matrix.CreateTranslation(-3, 0, -15); 
    DrawModel(carModel, carTextures, car1Matrix, technique, false); 
    
    Matrix car2Matrix = Matrix.CreateScale(4f) * Matrix.CreateRotationY(MathHelper.Pi * 5.0f / 8.0f) * Matrix.CreateTranslation(-28, 0, -1.9f); 
    DrawModel(carModel, carTextures, car2Matrix, technique, false); 
    
    Matrix lamp1Matrix = Matrix.CreateScale(0.05f) * Matrix.CreateTranslation(4.0f, 1, -35); 
    DrawModel(lamppostModel, lamppostTextures, lamp1Matrix, technique, true); 
    
    Matrix lamp2Matrix = Matrix.CreateScale(0.05f) * Matrix.CreateTranslation(4.0f, 1, -5); 
    DrawModel(lamppostModel, lamppostTextures, lamp2Matrix, technique, true); 
} 

这个方法使用参数中指定的technique绘制街道,2辆汽车和2个路灯,在Draw方法中调用这个方法:

protected override void Draw(GameTime gameTime) 
{ 
    device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); 
    
    DrawScene("ShadowMap"); 
    
    base.Draw(gameTime); 
}

现在当你运行代码时,你会看到如下图所示的图片。第一眼看起来很奇怪,但它是很完美的:这张图像就是从汽车的头灯看起来的场景的深度贴图。像素的颜色越白,离开头灯的距离越远。

程序截图

HLSL文件如下:

float4x4 xLightsWorldViewProjection; 
float4x4 xWorldViewProjection; 
float4x4 xWorld; 
bool xSolidBrown; 
float3 xLightPos; 
float xLightPower; 
float xAmbient; 
Texture xTexture; 
sampler TextureSampler = sampler_state { texture =  ; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;}; 

struct VertexToPixel 
{
    float4 Position : POSITION; 
    float2 TexCoords : TEXCOORD0; 
    float3 Normal : TEXCOORD1; 
    float3 Position3D : TEXCOORD2; 
}; 

struct PixelToFrame 
{
    float4 Color : COLOR0; 
};

VertexToPixel SimplestVertexShader( float4 inPos : POSITION0, float3 inNormal: NORMAL0, float2 inTexCoords : TEXCOORD0) 
{
    VertexToPixel Output = (VertexToPixel)0; 
    
    Output.Position =mul(inPos, xWorldViewProjection); 
    Output.TexCoords = inTexCoords; 
    Output.Normal = normalize(mul(inNormal, (float3x3)xWorld)); 
    Output.Position3D = mul(inPos, xWorld); 
    
    return Output; 
}

float DotProduct(float3 lightPos, float3 pos3D, float3 normal) 
{
    float3 lightDir = normalize(pos3D - lightPos); 
    return dot(-lightDir, normal); 
}

PixelToFrame OurFirstPixelShader(VertexToPixel PSIn) 
{
    PixelToFrame Output = (PixelToFrame)0; 
    
    float diffuseLightingFactor = DotProduct(xLightPos, PSIn.Position3D, PSIn.Normal); 
    diffuseLightingFactor = saturate(diffuseLightingFactor); 
    diffuseLightingFactor *= xLightPower; float4 baseColor = tex2D(TextureSampler, PSIn.TexCoords); 
    
    if (xSolidBrown == true) 
        baseColor = float4(0.25f, 0.21f, 0.20f, 1); 
    
    Output.Color = baseColor*(diffuseLightingFactor + xAmbient); 
    
    return Output; 
}

technique Simplest 
{
    pass Pass0 
    {
        VertexShader = compile vs_2_0 SimplestVertexShader(); 
        PixelShader = compile ps_2_0 OurFirstPixelShader(); 
    }
} 

//////////////////////////////////////////////////////////////////////////////////////////////////// 
struct SMapVertexToPixel 
{
    float4 Position : POSITION; 
    float4 Position2D : TEXCOORD0; 
}; 

struct SMapPixelToFrame 
{
    float4 Color : COLOR0; 
};

SMapVertexToPixel ShadowMapVertexShader( float4 inPos : POSITION) 
{
    SMapVertexToPixel Output = (SMapVertexToPixel)0; 
    
    Output.Position = mul(inPos, xLightsWorldViewProjection); 
    Output.Position2D = Output.Position; 
    
    return Output; 
}

SMapPixelToFrame ShadowMapPixelShader(SMapVertexToPixel PSIn) 
{
    SMapPixelToFrame Output = (SMapPixelToFrame)0; 
    
    Output.Color = PSIn.Position2D.z/PSIn.Position2D.w; 
    
    return Output; 
}

technique ShadowMap 
{
    pass Pass0 
    {
        VertexShader = compile vs_2_0 ShadowMapVertexShader(); 
        PixelShader = compile ps_2_0 ShadowMapPixelShader(); 
    }
}

XNA代码如下:

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace XNAseries3
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        struct MyOwnVertexFormat
        {
            private Vector3 position;
            private Vector2 texCoord;
            private Vector3 normal;

            public MyOwnVertexFormat(Vector3 position, Vector2 texCoord, Vector3 normal)
            {
                this.position = position;
                this.texCoord = texCoord;
                this.normal = normal;
            }

            public static VertexElement[] VertexElements =
            {
                new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0),
                new VertexElement(0, sizeof(float)*3, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0),
                new VertexElement(0, sizeof(float)*5, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0),
            };

            public static int SizeInBytes = sizeof(float) * (3 + 2 + 3);
        }

        GraphicsDeviceManager graphics;
        GraphicsDevice device;
        
        Effect effect;
        Matrix viewMatrix;
        Matrix projectionMatrix;
        VertexBuffer vertexBuffer;
        VertexDeclaration vertexDeclaration;
        Vector3 cameraPos;

        Texture2D streetTexture;
        Model lamppostModel;
        Texture2D[] lamppostTextures;
        Model carModel;
        Texture2D[] carTextures;

        Vector3 lightPos;
        float lightPower;
        float ambientPower;

        Matrix lightsViewProjectionMatrix;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);            
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            graphics.PreferredBackBufferWidth = 500;
            graphics.PreferredBackBufferHeight = 500;
            graphics.IsFullScreen = false;
            graphics.ApplyChanges();
            Window.Title = "Riemer's XNA Tutorials -- Series 3";

            base.Initialize();
        }

        protected override void LoadContent()
        {
            device = GraphicsDevice;

           effect = Content.Load ("OurHLSLfile");
           streetTexture = Content.Load ("streettexture");
           carModel = LoadModel("racer", out carTextures);
           lamppostModel = LoadModel("lamppost", out lamppostTextures);

           SetUpVertices();
           SetUpCamera();
       }

       private Model LoadModel(string assetName, out Texture2D[] textures)
       {

           Model newModel = Content.Load (assetName);
           textures = new Texture2D[7];
           int i = 0;
           foreach (ModelMesh mesh in newModel.Meshes)
               foreach (BasicEffect currentEffect in mesh.Effects)
                   textures[i++] = currentEffect.Texture;

           foreach (ModelMesh mesh in newModel.Meshes)
               foreach (ModelMeshPart meshPart in mesh.MeshParts)
                   meshPart.Effect = effect.Clone(device);

           return newModel;
       }

       private void SetUpVertices()
       {
           MyOwnVertexFormat[] vertices = new MyOwnVertexFormat[18];

           vertices[0] = new MyOwnVertexFormat(new Vector3(-20, 0, 10), new Vector2(-0.25f, 25.0f), new Vector3(0, 1, 0));
           vertices[1] = new MyOwnVertexFormat(new Vector3(-20, 0, -100), new Vector2(-0.25f, 0.0f), new Vector3(0, 1, 0));
           vertices[2] = new MyOwnVertexFormat(new Vector3(2, 0, 10), new Vector2(0.25f, 25.0f), new Vector3(0, 1, 0));
           vertices[3] = new MyOwnVertexFormat(new Vector3(2, 0, -100), new Vector2(0.25f, 0.0f), new Vector3(0, 1, 0));
           vertices[4] = new MyOwnVertexFormat(new Vector3(2, 0, 10), new Vector2(0.25f, 25.0f), new Vector3(-1, 0, 0));
           vertices[5] = new MyOwnVertexFormat(new Vector3(2, 0, -100), new Vector2(0.25f, 0.0f), new Vector3(-1, 0, 0));
           vertices[6] = new MyOwnVertexFormat(new Vector3(2, 1, 10), new Vector2(0.375f, 25.0f), new Vector3(-1, 0, 0));
           vertices[7] = new MyOwnVertexFormat(new Vector3(2, 1, -100), new Vector2(0.375f, 0.0f), new Vector3(-1, 0, 0));
           vertices[8] = new MyOwnVertexFormat(new Vector3(2, 1, 10), new Vector2(0.375f, 25.0f), new Vector3(0, 1, 0));
           vertices[9] = new MyOwnVertexFormat(new Vector3(2, 1, -100), new Vector2(0.375f, 0.0f), new Vector3(0, 1, 0));
           vertices[10] = new MyOwnVertexFormat(new Vector3(3, 1, 10), new Vector2(0.5f, 25.0f), new Vector3(0, 1, 0));
           vertices[11] = new MyOwnVertexFormat(new Vector3(3, 1, -100), new Vector2(0.5f, 0.0f), new Vector3(0, 1, 0));
           vertices[12] = new MyOwnVertexFormat(new Vector3(13, 1, 10), new Vector2(0.75f, 25.0f), new Vector3(0, 1, 0));
           vertices[13] = new MyOwnVertexFormat(new Vector3(13, 1, -100), new Vector2(0.75f, 0.0f), new Vector3(0, 1, 0));
           vertices[14] = new MyOwnVertexFormat(new Vector3(13, 1, 10), new Vector2(0.75f, 25.0f), new Vector3(-1, 0, 0));
           vertices[15] = new MyOwnVertexFormat(new Vector3(13, 1, -100), new Vector2(0.75f, 0.0f), new Vector3(-1, 0, 0));
           vertices[16] = new MyOwnVertexFormat(new Vector3(13, 21, 10), new Vector2(1.25f, 25.0f), new Vector3(-1, 0, 0));
           vertices[17] = new MyOwnVertexFormat(new Vector3(13, 21, -100), new Vector2(1.25f, 0.0f), new Vector3(-1, 0, 0));

           vertexBuffer = new VertexBuffer(device, vertices.Length * MyOwnVertexFormat.SizeInBytes, BufferUsage.WriteOnly);
           vertexBuffer.SetData(vertices);

           vertexDeclaration = new VertexDeclaration(device, MyOwnVertexFormat.VertexElements);
       }

       private void SetUpCamera()
       {
           cameraPos = new Vector3(-25, 13, 18);
           viewMatrix = Matrix.CreateLookAt(cameraPos, new Vector3(0, 2, -12), new Vector3(0, 1, 0));
           projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 1.0f, 200.0f);
       }

       protected override void UnloadContent()
       {
       }

       protected override void Update(GameTime gameTime)
       {            
           if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
               this.Exit();

           UpdateLightData();

           base.Update(gameTime);
       }

       private void UpdateLightData()
       {
           ambientPower = 0.2f;

           lightPos = new Vector3(-18, 5, -2);
           lightPower = 1.0f;


            Matrix lightsView = Matrix.CreateLookAt(lightPos, new Vector3(-2, 3, -10), new Vector3(0, 1, 0));
            Matrix lightsProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver2, 1f, 5f, 100f);

            lightsViewProjectionMatrix = lightsView* lightsProjection;
        }

        protected override void Draw(GameTime gameTime)
        {
            device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);
            DrawScene("ShadowMap");

            base.Draw(gameTime);
        }

        private void DrawScene(string technique)
        {
            effect.CurrentTechnique = effect.Techniques[technique];
            effect.Parameters["xWorldViewProjection"].SetValue(Matrix.Identity * viewMatrix * projectionMatrix);
            effect.Parameters["xTexture"].SetValue(streetTexture);
            effect.Parameters["xWorld"].SetValue(Matrix.Identity);
            effect.Parameters["xLightPos"].SetValue(lightPos);
            effect.Parameters["xLightPower"].SetValue(lightPower);
            effect.Parameters["xAmbient"].SetValue(ambientPower);
            effect.Parameters["xLightsWorldViewProjection"].SetValue(Matrix.Identity * lightsViewProjectionMatrix);
            effect.Begin();
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Begin();
                device.VertexDeclaration = vertexDeclaration;
                device.Vertices[0].SetSource(vertexBuffer, 0, MyOwnVertexFormat.SizeInBytes);
                device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 18);
                pass.End();
            }
            effect.End();

            Matrix car1Matrix = Matrix.CreateScale(4f) * Matrix.CreateRotationY(MathHelper.Pi) * Matrix.CreateTranslation(-3, 0, -15);
            DrawModel(carModel, carTextures, car1Matrix, technique, false);

            Matrix car2Matrix = Matrix.CreateScale(4f) * Matrix.CreateRotationY(MathHelper.Pi * 5.0f / 8.0f) * Matrix.CreateTranslation(-28, 0, -1.9f);
            DrawModel(carModel, carTextures, car2Matrix, technique, false);

            Matrix lamp1Matrix = Matrix.CreateScale(0.05f) * Matrix.CreateTranslation(4.0f, 1, -35);
            DrawModel(lamppostModel, lamppostTextures, lamp1Matrix, technique, true);

            Matrix lamp2Matrix = Matrix.CreateScale(0.05f) * Matrix.CreateTranslation(4.0f, 1, -5);
            DrawModel(lamppostModel, lamppostTextures, lamp2Matrix, technique, true);
        }

        private void DrawModel(Model model, Texture2D[] textures, Matrix wMatrix, string technique, bool solidBrown)
        {
            Matrix[] modelTransforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(modelTransforms);
            int i = 0;
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (Effect currentEffect in mesh.Effects)
                {
                    Matrix worldMatrix = modelTransforms[mesh.ParentBone.Index] * wMatrix;
                    currentEffect.CurrentTechnique = currentEffect.Techniques[technique];                    
                    currentEffect.Parameters["xWorldViewProjection"].SetValue(worldMatrix * viewMatrix * projectionMatrix);
                    currentEffect.Parameters["xTexture"].SetValue(textures[i++]);
                    currentEffect.Parameters["xSolidBrown"].SetValue(solidBrown);
                    currentEffect.Parameters["xWorld"].SetValue(worldMatrix);
                    currentEffect.Parameters["xLightPos"].SetValue(lightPos);
                    currentEffect.Parameters["xLightPower"].SetValue(lightPower);
                    currentEffect.Parameters["xAmbient"].SetValue(ambientPower);
                    currentEffect.Parameters["xLightsWorldViewProjection"].SetValue(worldMatrix * lightsViewProjectionMatrix);
                }
                mesh.Draw();
            }
        }

    }
}
文件下载(已下载 1169 次)

发布时间:2010/5/24 上午10:23:30  阅读次数:7469

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号