提交 93cb120e 编写于 作者: M Mr.doob

Merge remote-tracking branch 'alteredq/dev' into dev

......@@ -6,7 +6,7 @@
* Similar implementation to the one used by Crytek for CryEngine 2 [Sousa2008].
* Blurs a mask generated from the depth map along radial lines emanating from the light
* source. The blur repeatedly applies a blur filter of increasing support but constant
* sample count, to produce a blur filter with large support.
* sample count to produce a blur filter with large support.
*
* My implementation performs 3 passes, similar to the implementation from Sousa. I found
* just 6 samples per pass produced acceptible results. The blur is applied three times,
......@@ -42,31 +42,42 @@ THREE.ShaderGodRays = {
'godrays_generate': {
uniforms: {
tInput: {
type: "t",
value: 0,
texture: null
},
fStepSize: {
type: "f",
value: 1.0
},
vSunPositionScreenSpace: {
type: "v2",
value: new THREE.Vector2( 0.5, 0.5 )
}
},
vertexShader: [
"varying vec2 vUv;",
"void main() {",
"vUv = vec2( uv.x, 1.0 - uv.y );",
"gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
"}"
].join("\n"),
fragmentShader: [
"#define TAPS_PER_PASS 6.0",
"varying vec2 vUv;",
"uniform sampler2D tInput;",
......@@ -74,15 +85,19 @@ THREE.ShaderGodRays = {
"uniform vec2 vSunPositionScreenSpace;",
"uniform float fStepSize;", // filter step size
"#define TAPS_PER_PASS 6.0",
"void main() {",
// delta from current pixel to "sun" position
"vec2 delta = (vSunPositionScreenSpace - vUv);",
"float dist = length(delta);",
"vec2 delta = vSunPositionScreenSpace - vUv;",
"float dist = length( delta );",
// Step vector (uv space)
"vec2 stepv = fStepSize*delta/dist;",
"vec2 stepv = fStepSize * delta / dist;",
// Number of iterations between pixel and sun
"float iters = dist/fStepSize;",
"vec2 uv = vUv.xy;",
......@@ -90,16 +105,20 @@ THREE.ShaderGodRays = {
// Unrolling didnt do much on my hardware (ATI Mobility Radeon 3450),
// so i've just left the loop
"for (float i = 0.0; i < TAPS_PER_PASS; i+=1.0 ) {",
"for ( float i = 0.0; i < TAPS_PER_PASS; i += 1.0 ) {",
// Accumulate samples, making sure we dont walk past the light source.
// The check for uv.y<1 would not be necessary with "border" UV wrap
// The check for uv.y < 1 would not be necessary with "border" UV wrap
// mode, with a black border colour. I don't think this is currently
// exposed by three.js. As a result there might be artifacts when the
// sun is to the left, right or bottom of screen as these cases are
// not specifically handled.
"col += (i <= iters && uv.y<1. ? texture2D( tInput, uv ).r : .0) ;",
"col += ( i <= iters && uv.y < 1.0 ? texture2D( tInput, uv ).r : 0.0 );",
"uv += stepv;",
"}",
// Should technically be dividing by 'iters', but 'TAPS_PER_PASS' smooths out
......@@ -108,8 +127,10 @@ THREE.ShaderGodRays = {
// TAPS_PER_PASS is greater than the number of samples actually accumulated.
// When the result is inverted (in the shader 'godrays_combine', this produces
// a slight bright spot at the position of the sun, even when it is occluded.
"gl_FragColor = vec4( col/TAPS_PER_PASS );",
"gl_FragColor.a = 1.;",
"gl_FragColor.a = 1.0;",
"}"
].join("\n")
......@@ -120,35 +141,46 @@ THREE.ShaderGodRays = {
* Additively applies god rays from texture tGodRays to a background (tColors).
* fGodRayIntensity attenuates the god rays.
*/
'godrays_combine': {
uniforms: {
tColors: {
type: "t",
value: 0,
texture: null
},
tGodRays: {
type: "t",
value: 1,
texture: null
},
fGodRayIntensity: {
type: "f",
value: 0.69
},
vSunPositionScreenSpace: {
type: "v2",
value: new THREE.Vector2( 0.5, 0.5 )
},
}
},
vertexShader: [
"varying vec2 vUv;",
"void main() {",
"vUv = vec2( uv.x, 1.0 - uv.y );",
"gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
"}"
].join("\n"),
fragmentShader: [
......@@ -162,41 +194,63 @@ THREE.ShaderGodRays = {
"uniform float fGodRayIntensity;",
"void main() {",
// Since THREE.MeshDepthMaterial renders foreground objects white and background
// objects black, the god-rays will be white streaks. Therefore value is inverted
// before being combined with tColors
"gl_FragColor = texture2D( tColors, vUv ) + fGodRayIntensity*vec4( 1.-texture2D( tGodRays, vUv ).r );",
"gl_FragColor.a = 1.;",
"gl_FragColor = texture2D( tColors, vUv ) + fGodRayIntensity * vec4( 1.0 - texture2D( tGodRays, vUv ).r );",
"gl_FragColor.a = 1.0;",
"}"
].join("\n")
},
/**
* A dodgy sun/sky shader. Makes a bright spot at the sun location. Would be
* cheaper/faster/simpler to implement this as a simple sun sprite.
*/
'godrays_fake_sun': {
uniforms: {
vSunPositionScreenSpace: {
type: "v2",
value: new THREE.Vector2( 0.5, 0.5 )
},
fAspect: {
type: "f",
value: 1.0
},
sunColor: {
type: "c",
value: new THREE.Color( 0xffee00 )
},
bgColor: {
type: "c",
value: new THREE.Color( 0x000000 )
}
},
vertexShader: [
"varying vec2 vUv;",
"void main() {",
"vUv = vec2( uv.x, 1.0 - uv.y );",
"gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
"}"
].join("\n"),
fragmentShader: [
......@@ -206,13 +260,23 @@ THREE.ShaderGodRays = {
"uniform vec2 vSunPositionScreenSpace;",
"uniform float fAspect;",
"uniform vec3 sunColor;",
"uniform vec3 bgColor;",
"void main() {",
"vec2 diff = vUv-vSunPositionScreenSpace;",
"vec2 diff = vUv - vSunPositionScreenSpace;",
// Correct for aspect ratio
"diff.x *= fAspect;",
"float prop = clamp(length(diff)/.5,0.,1.);",
"prop = .35*pow( 1.0 - prop, 3. ) ;",
"gl_FragColor = vec4(prop,prop,0.2,1.);",
"float prop = clamp( length( diff ) / 0.5, 0.0, 1.0 );",
"prop = 0.35 * pow( 1.0 - prop, 3.0 );",
"gl_FragColor.xyz = mix( sunColor, bgColor, 1.0 - prop );",
"gl_FragColor.w = 1.0;",
"}"
].join("\n")
......
......@@ -32,9 +32,9 @@
<body>
<script src="../build/Three.js"></script>
<script src="js/ShaderGodRays.js"></script>
<script src="js/Detector.js"></script>
<script src="js/ShaderGodRays.js"></script>
<script src="js/Stats.js"></script>
<div id="info">
......@@ -47,21 +47,29 @@
if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
var container, stats;
var camera, scene, renderer, parameters, material_depth;
var tree_mesh, sphere_mesh;
var camera, scene, renderer, materialDepth;
var treeMesh, sphereMesh;
var proj = new THREE.Projector();
var sunPos = new THREE.Vector3( 0, 1000, -1000 );
var projector = new THREE.Projector();
var sunPosition = new THREE.Vector3( 0, 1000, -1000 );
var screenSpacePosition = new THREE.Vector3();
var mouseX = 0, mouseY = 0;
var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;
var height = window.innerHeight-100;
var margin = 100;
var height = window.innerHeight - 2 * margin;
var postprocessing = { enabled : true };
var orbitRadius = 200;
var postprocessing = { enabled : true };
var bgColor = 0x000511;
var sunColor = 0xffee00;
init();
animate();
......@@ -71,164 +79,194 @@
container = document.createElement( 'div' );
document.body.appendChild( container );
//
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / height, 1, 3000 );
camera.position.z = 200;
scene.add( camera );
renderer = new THREE.WebGLRenderer( { antialias: false } );
renderer.setSize( window.innerWidth, height );
container.appendChild( renderer.domElement );
renderer.sortObjects = false;
//
// todo - try with fog
//scene.fog = new THREE.Fog( 0xffaa55, 1000, FAR );
//THREE.ColorUtils.adjustHSV( scene.fog.color, 0.02, -0.15, -0.65 );
material_depth = new THREE.MeshDepthMaterial();
materialDepth = new THREE.MeshDepthMaterial();
var materialScene = new THREE.MeshBasicMaterial( { color: 0x000000, shading: THREE.FlatShading } );
// tree
parameters = { color: 0x000000, shading: THREE.FlatShading };
var zmat = new THREE.MeshBasicMaterial( parameters );
var loader = new THREE.JSONLoader();
// tree mesh
var jsonLoader = new THREE.JSONLoader();
jsonLoader.load( "obj/tree/tree.js", function( geometry ) {
loader.load( "obj/tree/tree.js", function( geometry ) {
var m = new THREE.Mesh( geometry, zmat );
m.position.set( 0, -150, -150 );
treeMesh = new THREE.Mesh( geometry, materialScene );
treeMesh.position.set( 0, -150, -150 );
var sc = 400;
m.scale.set( sc, sc, sc );
m.matrixAutoUpdate = false;
m.updateMatrix();
scene.add( m );
tree_mesh = m;
treeMesh.scale.set( sc, sc, sc );
treeMesh.matrixAutoUpdate = false;
treeMesh.updateMatrix();
scene.add( treeMesh );
} );
// sphere
var geo = new THREE.SphereGeometry( 1, 20, 10 );
mesh = new THREE.Mesh( geo, zmat );
sphereMesh = new THREE.Mesh( geo, materialScene );
var sc = 20;
mesh.scale.set( sc, sc, sc );
sphereMesh.scale.set( sc, sc, sc );
mesh.matrixAutoUpdate = false;
mesh.updateMatrix();
scene.add( sphereMesh );
scene.add( mesh );
sphere_mesh = mesh;
scene.matrixAutoUpdate = false;
//
initPostprocessing();
renderer = new THREE.WebGLRenderer( { antialias: false } );
renderer.setSize( window.innerWidth, height );
container.appendChild( renderer.domElement );
renderer.sortObjects = false;
renderer.autoClear = false;
renderer.setClearColorHex( 0x000033, 1);
renderer.setClearColorHex( bgColor, 1 );
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = "50px";
renderer.domElement.style.top = margin + "px";
renderer.domElement.style.left = "0px";
//
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
container.appendChild( stats.domElement );
stats.domElement.children[ 0 ].children[ 0 ].style.color = "#888";
stats.domElement.children[ 0 ].style.background = "transparent";
stats.domElement.children[ 0 ].children[ 1 ].style.display = "none";
//
document.addEventListener( 'mousemove', onDocumentMouseMove, false );
document.addEventListener( 'touchstart', onDocumentTouchStart, false );
document.addEventListener( 'touchmove', onDocumentTouchMove, false );
//
initPostprocessing();
}
//
function onDocumentMouseMove( event ) {
mouseX = event.clientX - windowHalfX;
mouseY = event.clientY - windowHalfY;
}
function onDocumentTouchStart( event ) {
if ( event.touches.length == 1 ) {
if ( event.touches.length === 1 ) {
event.preventDefault();
mouseX = event.touches[ 0 ].pageX - windowHalfX;
mouseY = event.touches[ 0 ].pageY - windowHalfY;
}
}
function onDocumentTouchMove( event ) {
if ( event.touches.length == 1 ) {
if ( event.touches.length === 1 ) {
event.preventDefault();
mouseX = event.touches[ 0 ].pageX - windowHalfX;
mouseY = event.touches[ 0 ].pageY - windowHalfY;
}
}
//
function initPostprocessing() {
postprocessing.scene = new THREE.Scene();
postprocessing.camera = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, -10000, 10000 );
postprocessing.camera = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, height / 2, height / - 2, -10000, 10000 );
postprocessing.camera.position.z = 100;
postprocessing.scene.add( postprocessing.camera );
var pars = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat };
postprocessing.rtTextureColors = new THREE.WebGLRenderTarget( window.innerWidth, height, pars );
// Switching the depth formats to luminance from rgb doesn't seem to work. I didn't
// investigate further for now.
// pars.format = THREE.LuminanceFormat;
// I would have this quarter size and use it as one of the ping-pong render
// targets but the aliasing causes some temporal flickering
postprocessing.rtTextureDepth = new THREE.WebGLRenderTarget( window.innerWidth, height, pars );
// Aggressive downsize god-ray ping-pong render targets to minimize cost
// Aggressive downsize god-ray ping-pong render targets to minimize cost
var w = window.innerWidth / 4.0;
var h = height / 4.0;
postprocessing.rtTextureGodRays1 = new THREE.WebGLRenderTarget( w, h, pars );
postprocessing.rtTextureGodRays2 = new THREE.WebGLRenderTarget( w, h, pars );
// god-ray shaders
var godray_gen_shader = THREE.ShaderGodRays[ "godrays_generate" ];
postprocessing.godray_gen_uniforms = THREE.UniformsUtils.clone( godray_gen_shader.uniforms );
var godraysGenShader = THREE.ShaderGodRays[ "godrays_generate" ];
postprocessing.godrayGenUniforms = THREE.UniformsUtils.clone( godraysGenShader.uniforms );
postprocessing.materialGodraysGenerate = new THREE.ShaderMaterial( {
uniforms: postprocessing.godray_gen_uniforms,
vertexShader: godray_gen_shader.vertexShader,
fragmentShader: godray_gen_shader.fragmentShader
uniforms: postprocessing.godrayGenUniforms,
vertexShader: godraysGenShader.vertexShader,
fragmentShader: godraysGenShader.fragmentShader
} );
var godrays_combine_shader = THREE.ShaderGodRays[ "godrays_combine" ];
postprocessing.godray_combine_uniforms = THREE.UniformsUtils.clone( godrays_combine_shader.uniforms );
var godraysCombineShader = THREE.ShaderGodRays[ "godrays_combine" ];
postprocessing.godrayCombineUniforms = THREE.UniformsUtils.clone( godraysCombineShader.uniforms );
postprocessing.materialGodraysCombine = new THREE.ShaderMaterial( {
uniforms: postprocessing.godray_combine_uniforms,
vertexShader: godrays_combine_shader.vertexShader,
fragmentShader: godrays_combine_shader.fragmentShader
uniforms: postprocessing.godrayCombineUniforms,
vertexShader: godraysCombineShader.vertexShader,
fragmentShader: godraysCombineShader.fragmentShader
} );
var godrays_fake_sun_shader = THREE.ShaderGodRays[ "godrays_fake_sun" ];
postprocessing.godrays_fake_sun_uniforms = THREE.UniformsUtils.clone( godrays_fake_sun_shader.uniforms );
var godraysFakeSunShader = THREE.ShaderGodRays[ "godrays_fake_sun" ];
postprocessing.godraysFakeSunUniforms = THREE.UniformsUtils.clone( godraysFakeSunShader.uniforms );
postprocessing.materialGodraysFakeSun = new THREE.ShaderMaterial( {
uniforms: postprocessing.godrays_fake_sun_uniforms,
vertexShader: godrays_fake_sun_shader.vertexShader,
fragmentShader: godrays_fake_sun_shader.fragmentShader
uniforms: postprocessing.godraysFakeSunUniforms,
vertexShader: godraysFakeSunShader.vertexShader,
fragmentShader: godraysFakeSunShader.fragmentShader
} );
postprocessing.quad = new THREE.Mesh( new THREE.PlaneGeometry( window.innerWidth, window.innerHeight ), postprocessing.materialGodraysGenerate );
postprocessing.godraysFakeSunUniforms.bgColor.value.setHex( bgColor );
postprocessing.godraysFakeSunUniforms.sunColor.value.setHex( sunColor );
postprocessing.godrayCombineUniforms.fGodRayIntensity.value = 0.75;
postprocessing.quad = new THREE.Mesh( new THREE.PlaneGeometry( window.innerWidth, height ), postprocessing.materialGodraysGenerate );
postprocessing.quad.position.z = -9900;
postprocessing.quad.rotation.x = Math.PI / 2;
postprocessing.scene.add( postprocessing.quad );
......@@ -238,7 +276,7 @@
function animate() {
requestAnimationFrame( animate, renderer.domElement );
render();
stats.update();
......@@ -246,66 +284,81 @@
function render() {
if ( sphere_mesh ) {
var time = Date.now() / 4000;
var radius = 100;
sphere_mesh.position.x = 200*Math.cos(Date.now()/4000);
sphere_mesh.position.z = 200*Math.sin(Date.now()/4000)-100;
sphere_mesh.updateMatrix();
sphereMesh.position.x = orbitRadius * Math.cos( time );
sphereMesh.position.z = orbitRadius * Math.sin( time ) - 100;
}
camera.position.x += ( mouseX - camera.position.x ) * 0.036;
camera.position.y += ( - (mouseY) - camera.position.y ) * 0.036;
camera.position.y += ( - ( mouseY ) - camera.position.y ) * 0.036;
camera.lookAt( scene.position );
if ( postprocessing.enabled ) {
// Find the screenspace position of the sun
var sspos = new THREE.Vector3( sunPos.x, sunPos.y, sunPos.z);
proj.projectVector(sspos,camera);
sspos.x = (sspos.x+1)/2;
sspos.y = (sspos.y+1)/2;
screenSpacePosition.copy( sunPosition );
projector.projectVector( screenSpacePosition, camera );
screenSpacePosition.x = ( screenSpacePosition.x + 1 ) / 2;
screenSpacePosition.y = ( screenSpacePosition.y + 1 ) / 2;
// Give it to the god-ray and sun shaders
postprocessing.godray_gen_uniforms[ "vSunPositionScreenSpace" ].value = new THREE.Vector2(sspos.x,sspos.y);
postprocessing.godrays_fake_sun_uniforms[ "vSunPositionScreenSpace" ].value = new THREE.Vector2(sspos.x,sspos.y);
postprocessing.godrayGenUniforms[ "vSunPositionScreenSpace" ].value.x = screenSpacePosition.x;
postprocessing.godrayGenUniforms[ "vSunPositionScreenSpace" ].value.y = screenSpacePosition.y;
postprocessing.godraysFakeSunUniforms[ "vSunPositionScreenSpace" ].value.x = screenSpacePosition.x;
postprocessing.godraysFakeSunUniforms[ "vSunPositionScreenSpace" ].value.y = screenSpacePosition.y;
// -- Draw sky and sun --
// Clear colors and depths, will clear to sky color
renderer.clearTarget(postprocessing.rtTextureColors,true,true,false);
renderer.clearTarget( postprocessing.rtTextureColors, true, true, false );
// Sun render. Runs a shader that gives a brightness based on the screen
// space distance to the sun. Not very efficient, so i make a scissor
// rect around the suns position to avoid rendering surrounding pixels.
var sunsqH = 0.74 * height; // .74 depends on extent of sun from shader
var sunsqW = 0.74 * height; // both dep on height because sun is aspect-corrected
sspos.x *= window.innerWidth;
sspos.y *= height;
renderer.setScissor( sspos.x-sunsqW/2,sspos.y-sunsqH/2,sunsqW,sunsqH );
// rectangle around the suns position to avoid rendering surrounding pixels.
var sunsqH = 0.74 * height; // 0.74 depends on extent of sun from shader
var sunsqW = 0.74 * height; // both depend on height because sun is aspect-corrected
screenSpacePosition.x *= window.innerWidth;
screenSpacePosition.y *= height;
renderer.setScissor( screenSpacePosition.x - sunsqW / 2, screenSpacePosition.y - sunsqH / 2, sunsqW, sunsqH );
renderer.enableScissorTest( true );
postprocessing.godrays_fake_sun_uniforms[ "fAspect" ].value = window.innerWidth / height;
postprocessing.godraysFakeSunUniforms[ "fAspect" ].value = window.innerWidth / height;
postprocessing.scene.overrideMaterial = postprocessing.materialGodraysFakeSun;
renderer.render( postprocessing.scene, postprocessing.camera, postprocessing.rtTextureColors );
renderer.enableScissorTest( false );
// -- Draw scene objects --
// Colors
scene.overrideMaterial = null;
renderer.render( scene, camera, postprocessing.rtTextureColors );
// Depth
scene.overrideMaterial = material_depth;
scene.overrideMaterial = materialDepth;
renderer.render( scene, camera, postprocessing.rtTextureDepth, true );
// -- Render god-rays --
// Maximum length of god-rays (in texture space [0,1]X[0,1])
var filterLen = 1.0;
// Samples taken by filter
var TAPS_PER_PASS = 6.0;
// Pass order could equivalently be 3,2,1 (instead of 1,2,3), which
......@@ -314,31 +367,44 @@
// appear as a glimmer along the length of the beams
// pass 1 - render into first ping-pong target
var pass = 1.0;
var stepLen = filterLen*Math.pow(TAPS_PER_PASS, -pass);
postprocessing.godray_gen_uniforms[ "fStepSize" ].value = stepLen;
postprocessing.godray_gen_uniforms[ "tInput" ].texture = postprocessing.rtTextureDepth;
var stepLen = filterLen * Math.pow( TAPS_PER_PASS, -pass );
postprocessing.godrayGenUniforms[ "fStepSize" ].value = stepLen;
postprocessing.godrayGenUniforms[ "tInput" ].texture = postprocessing.rtTextureDepth;
postprocessing.scene.overrideMaterial = postprocessing.materialGodraysGenerate;
renderer.render( postprocessing.scene, postprocessing.camera, postprocessing.rtTextureGodRays2 );
// pass 2 - render into second ping-pong target
pass = 2.0;
stepLen = filterLen*Math.pow(TAPS_PER_PASS, -pass);
postprocessing.godray_gen_uniforms[ "fStepSize" ].value = stepLen;
postprocessing.godray_gen_uniforms[ "tInput" ].texture = postprocessing.rtTextureGodRays2;
renderer.render( postprocessing.scene, postprocessing.camera , postprocessing.rtTextureGodRays1 );
stepLen = filterLen * Math.pow( TAPS_PER_PASS, -pass );
postprocessing.godrayGenUniforms[ "fStepSize" ].value = stepLen;
postprocessing.godrayGenUniforms[ "tInput" ].texture = postprocessing.rtTextureGodRays2;
renderer.render( postprocessing.scene, postprocessing.camera, postprocessing.rtTextureGodRays1 );
// pass 3 - 1st RT
pass = 3.0;
stepLen = filterLen*Math.pow(TAPS_PER_PASS, -pass);
postprocessing.godray_gen_uniforms[ "fStepSize" ].value = stepLen;
postprocessing.godray_gen_uniforms[ "tInput" ].texture = postprocessing.rtTextureGodRays1;
stepLen = filterLen * Math.pow( TAPS_PER_PASS, -pass );
postprocessing.godrayGenUniforms[ "fStepSize" ].value = stepLen;
postprocessing.godrayGenUniforms[ "tInput" ].texture = postprocessing.rtTextureGodRays1;
renderer.render( postprocessing.scene, postprocessing.camera , postprocessing.rtTextureGodRays2 );
// final pass - composite god-rays onto colors
postprocessing.godray_combine_uniforms["tColors"].texture = postprocessing.rtTextureColors;
postprocessing.godray_combine_uniforms["tGodRays"].texture = postprocessing.rtTextureGodRays2;
postprocessing.godrayCombineUniforms["tColors"].texture = postprocessing.rtTextureColors;
postprocessing.godrayCombineUniforms["tGodRays"].texture = postprocessing.rtTextureGodRays2;
postprocessing.scene.overrideMaterial = postprocessing.materialGodraysCombine;
renderer.render( postprocessing.scene, postprocessing.camera );
postprocessing.scene.overrideMaterial = null;
......@@ -350,6 +416,7 @@
}
}
</script>
</body>
</html>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册