如何计算正切和副法线?

发布于 2024-10-21 07:25:28 字数 859 浏览 1 评论 0原文

谈论凹凸贴图、镜面高光以及 OpenGL 着色语言 (GLSL) 中的此类内容,

我有:

  • 顶点数组(例如 {0.2,0.5,0.1, 0.2,0.4,0.5, ...})
  • 法线数组(例如 {0.0,0.0,1.0, 0.0,1.0,0.0, ...})
  • 点光源在世界空间中的位置(例如 {0.0,1.0,-5.0})
  • 观察者在世界空间中的位置(例如{0.0,0.0,0.0})(假设观看者位于世界中心)

现在,如何计算每个顶点的副法线和切线?我的意思是,计算双法线的公式是什么,我必须根据这些信息使用什么?关于切线?

无论如何,我都会构建 TBN 矩阵,因此,如果您知道直接根据这些信息构建矩阵的公式,那就太好了!

哦,是的,如果需要的话,我也有纹理坐标。 当我谈论 GLSL 时,如果有一种逐顶点解决方案会很好,我的意思是,这种解决方案不需要一次访问多个顶点信息。

---- 更新-----

我找到了这个解决方案:

vec3 tangent;
vec3 binormal;

vec3 c1 = cross(a_normal, vec3(0.0, 0.0, 1.0));
vec3 c2 = cross(a_normal, vec3(0.0, 1.0, 0.0));

if (length(c1)>length(c2))
{
    tangent = c1;
}
else
{
    tangent = c2;
}

tangent = normalize(tangent);

binormal = cross(v_nglNormal, tangent);
binormal = normalize(binormal);

但我不知道它是否100%正确。

Talking about bump mapping, specular highlight and these kind of things in OpenGL Shading Language (GLSL)

I have:

  • An array of vertices (e.g. {0.2,0.5,0.1, 0.2,0.4,0.5, ...})
  • An array of normals (e.g. {0.0,0.0,1.0, 0.0,1.0,0.0, ...})
  • The position of a point light in world space (e.g. {0.0,1.0,-5.0})
  • The position of the viewer in world space (e.g. {0.0,0.0,0.0}) (assume the viewer is in the center of the world)

Now, how can I calculate the Binormal and Tangent for each vertex? I mean, what is the formula to calculate the Binormals, what I have to use based on those informations? And about the tangent?

I'll construct the TBN Matrix anyway, so if you know a formula to construct the matrix directly based on those informations will be nice!

Oh, yeh, I have the texture coordinates too, if needed.
And as I'm talking about GLSL, would be nice a per-vertex solution, I mean, one which doesn't need to access more than one vertex information at a time.

---- Update -----

I found this solution:

vec3 tangent;
vec3 binormal;

vec3 c1 = cross(a_normal, vec3(0.0, 0.0, 1.0));
vec3 c2 = cross(a_normal, vec3(0.0, 1.0, 0.0));

if (length(c1)>length(c2))
{
    tangent = c1;
}
else
{
    tangent = c2;
}

tangent = normalize(tangent);

binormal = cross(v_nglNormal, tangent);
binormal = normalize(binormal);

But I don't know if it is 100% correct.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

萌化 2024-10-28 07:25:28

与您的问题相关的输入数据是纹理坐标。切线和副法线是局部平行于对象表面的向量。在法线贴图的情况下,它们描述的是法线纹理的局部方向。

因此,您必须计算纹理向量指向的方向(在模型空间中)。假设您有一个三角形 ABC,其纹理坐标为 HKL。这给了我们向量:

D = B-A
E = C-A

F = K-H
G = L-H

现在我们想用切线空间 T, U 来表达 D 和 E,即

D = F.s * T + F.t * U
E = G.s * T + G.t * U

这是一个有 6 个未知数和 6 个方程的线性方程组,它可以写为

| D.x D.y D.z |   | F.s F.t | | T.x T.y T.z |
|             | = |         | |             |
| E.x E.y E.z |   | G.s G.t | | U.x U.y U.z |

求逆 FG 矩阵得到

| T.x T.y T.z |           1         |  G.t  -F.t | | D.x D.y D.z |
|             | = ----------------- |            | |             |
| U.x U.y U.z |   F.s G.t - F.t G.s | -G.s   F.s | | E.x E.y E.z |

与顶点法线 T 和 U 形成局部空间基,称为切线空间,由矩阵描述

| T.x U.x N.x |
| T.y U.y N.y |
| T.z U.z N.z |

从切线空间变换到对象空间。为了进行照明计算,需要与此相反。经过一点练习,我们会发现:

T' = T - (N·T) N
U' = U - (N·U) N - (T'·U) T'

对向量 T' 和 U' 进行归一化,将它们称为切线和副法线,我们获得从对象转换到切线空间的矩阵,我们在切线空间中进行照明:

| T'.x T'.y T'.z |
| U'.x U'.y U'.z |
| N.x  N.y  N.z  |

我们将 T' 和 U' 存储在一起顶点法线作为模型几何体的一部分(作为顶点属性),以便我们可以在着色器中使用它们进行光照计算。 我再说一遍:您不需要在着色器中确定切线和副法线,您可以预先计算它们并将它们存储为模型几何体的一部分(就像法线一样)。

(上面垂直条之间的符号都是矩阵,绝不是行列式,通常在表示法中使用竖线而不是括号。)

The relevant input data to your problem are the texture coordinates. Tangent and Binormal are vectors locally parallel to the object's surface. And in the case of normal mapping they're describing the local orientation of the normal texture.

So you have to calculate the direction (in the model's space) in which the texturing vectors point. Say you have a triangle ABC, with texture coordinates HKL. This gives us vectors:

D = B-A
E = C-A

F = K-H
G = L-H

Now we want to express D and E in terms of tangent space T, U, i.e.

D = F.s * T + F.t * U
E = G.s * T + G.t * U

This is a system of linear equations with 6 unknowns and 6 equations, it can be written as

| D.x D.y D.z |   | F.s F.t | | T.x T.y T.z |
|             | = |         | |             |
| E.x E.y E.z |   | G.s G.t | | U.x U.y U.z |

Inverting the FG matrix yields

| T.x T.y T.z |           1         |  G.t  -F.t | | D.x D.y D.z |
|             | = ----------------- |            | |             |
| U.x U.y U.z |   F.s G.t - F.t G.s | -G.s   F.s | | E.x E.y E.z |

Together with the vertex normal T and U form a local space basis, called the tangent space, described by the matrix

| T.x U.x N.x |
| T.y U.y N.y |
| T.z U.z N.z |

Transforming from tangent space into object space. To do lighting calculations one needs the inverse of this. With a little bit of exercise one finds:

T' = T - (N·T) N
U' = U - (N·U) N - (T'·U) T'

Normalizing the vectors T' and U', calling them tangent and binormal we obtain the matrix transforming from object into tangent space, where we do the lighting:

| T'.x T'.y T'.z |
| U'.x U'.y U'.z |
| N.x  N.y  N.z  |

We store T' and U' them together with the vertex normal as a part of the model's geometry (as vertex attributes), so that we can use them in the shader for lighting calculations. I repeat: You don't determine tangent and binormal in the shader, you precompute them and store them as part of the model's geometry (just like normals).

(The notation between the vertical bars above are all matrices, never determinants, which normally use vertical bars instead of brackets in their notation.)

爱格式化 2024-10-28 07:25:28

通常,有 2 种生成 TBN 矩阵的方法:离线和在线。

  • 在线 = 直接在片段着色器中使用派生指令。这些推导为您提供了多边形每个点的平坦 TBN 基础。为了获得平滑的效果,我们必须根据给定的(平滑)顶点法线重新正交化它。此过程对 GPU 的负担甚至比初始 TBN 提取还要重。

     // 计算世界位置的导数
     vec3 p_dx = dFdx(pw_i);
     vec3 p_dy = dFdy(pw_i);
     // 计算纹理坐标的导数
     vec2 tc_dx = dFdx(tc_i);
     vec2 tc_dy = dFdy(tc_i);
     // 计算初始正切和双正切
     vec3 t = 归一化( tc_dy.y * p_dx - tc_dx.y * p_dy );
     vec3 b = 归一化( tc_dy.x * p_dx - tc_dx.x * p_dy ); // 符号反转
     // 从给定的网格法线获取新的切线
     vec3 n = 归一化(n_obj_i);
     vec3 x = 交叉(n, t);
     t = 交叉(x,n);
     t = 归一化(t);
     // 获取更新的双切线
     x = 交叉(b, n);
     b = 交叉(n,x);
     b = 归一化(b);
     mat3 tbn = mat3(t, b, n);
    
  • 离线 = 准备切线作为顶点属性。这更难实现,因为它不仅会添加另一个顶点属性,而且还需要重新组合所有其他属性。此外,它不会 100% 为您提供更好的性能,因为您将获得存储/传递/动画(!)vector3 顶点属性的额外成本。

数学在很多地方都有描述(谷歌一下),包括@datenwolf 帖子。

这里的问题是两个顶点可能具有相同的法线和纹理坐标,但切线不同。这意味着您不能只向顶点添加顶点属性,您需要将顶点拆分为 2 并为克隆指定不同的切线。

获得每个顶点唯一的切线(和其他属性)的最佳方法是尽早在导出器中执行此操作。在按属性对纯顶点进行排序的阶段,您只需将切向量添加到排序键即可。

作为该问题的根本解决方案,请考虑使用四元数。单个四元数 (vec4) 可以成功地表示预定义灵活性的切向空间。保持正交(包括传递到片段着色器)、存储和提取法线(如果需要)很容易。如需了解更多信息,请访问 KRI 维基

Generally, you have 2 ways of generating the TBN matrix: off-line and on-line.

  • On-line = right in the fragment shader using derivative instructions. Those derivations give you a flat TBN basis for each point of a polygon. In order to get a smooth one we have to re-orthogonalize it based on a given (smooth) vertex normal. This procedure is even more heavy on GPU than initial TBN extraction.

     // compute derivations of the world position
     vec3 p_dx = dFdx(pw_i);
     vec3 p_dy = dFdy(pw_i);
     // compute derivations of the texture coordinate
     vec2 tc_dx = dFdx(tc_i);
     vec2 tc_dy = dFdy(tc_i);
     // compute initial tangent and bi-tangent
     vec3 t = normalize( tc_dy.y * p_dx - tc_dx.y * p_dy );
     vec3 b = normalize( tc_dy.x * p_dx - tc_dx.x * p_dy ); // sign inversion
     // get new tangent from a given mesh normal
     vec3 n = normalize(n_obj_i);
     vec3 x = cross(n, t);
     t = cross(x, n);
     t = normalize(t);
     // get updated bi-tangent
     x = cross(b, n);
     b = cross(n, x);
     b = normalize(b);
     mat3 tbn = mat3(t, b, n);
    
  • Off-line = prepare tangent as a vertex attribute. This is more difficult to get because it will not just add another vertex attrib but also will require to re-compose all other attributes. Moreover, it will not 100% give you a better performance as you'll get an additional cost of storing/passing/animating(!) vector3 vertex attribute.

The math is described in many places (google it), including the @datenwolf post.

The problem here is that 2 vertices may have the same normal and texture coordinate but different tangents. That means you can not just add a vertex attribute to a vertex, you'll need to split the vertex into 2 and specify different tangents for the clones.

The best way to get unique tangent (and other attribs) per vertex is to do it as early as possible = in the exporter. There on the stage of sorting pure vertices by attributes you'll just need to add the tangent vector to the sorting key.

As a radical solution to the problem consider using quaternions. A single quaternion (vec4) can successfully represent tangential space of a pre-defined handiness. It's easy to keep orthonormal (including passing to the fragment shader), store and extract normal if needed. More info on the KRI wiki.

極樂鬼 2024-10-28 07:25:28

根据kvark的回答,我想补充更多的想法。

如果您需要正交化切线空间矩阵,您必须以任何方式做一些工作。
即使您添加切线和副法线属性,它们也会在着色器阶段进行插值
最终它们既没有被标准化,也没有彼此正常化。

假设我们有一个归一化法线向量 n,并且我们有切线t 和副法线b 或者我们可以通过如下推导来计算它们:

// derivations of the fragment position
vec3 pos_dx = dFdx( fragPos );
vec3 pos_dy = dFdy( fragPos );
// derivations of the texture coordinate
vec2 texC_dx = dFdx( texCoord );
vec2 texC_dy = dFdy( texCoord );
// tangent vector and binormal vector
vec3 t = texC_dy.y * pos_dx - texC_dx.y * pos_dy;
vec3 b = texC_dx.x * pos_dy - texC_dy.x * pos_dx;

当然可以使用叉积来计算正交化切空间矩阵,
但这仅适用于右手系统。如果矩阵被镜像(左手系统),它将变成右手系统:

t = cross( cross( n, t ), t ); // orthonormalization of the tangent vector
b = cross( n, t );             // orthonormalization of the binormal vector 
                               //   may invert the binormal vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

在上面的代码片段中,如果切线空间是左手系统,则副法线向量会反转。
为了避免这种情况,必须采取困难的方法:

t = cross( cross( n, t ), t ); // orthonormalization of the tangent vector
b = cross( b, cross( b, n ) ); // orthonormalization of the binormal vectors to the normal vector 
b = cross( cross( t, b ), t ); // orthonormalization of the binormal vectors to the tangent vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

正交化任何矩阵的常见方法是 Gram-Schmidt 过程

t = t - n * dot( t, n ); // orthonormalization ot the tangent vectors
b = b - n * dot( b, n ); // orthonormalization of the binormal vectors to the normal vector 
b = b - t * dot( b, t ); // orthonormalization of the binormal vectors to the tangent vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

另一种可能性是使用 2*2 矩阵的行列式,该矩阵由纹理坐标 texC_dxtexC_dy 的推导得出,考虑副法向量的方向。这个想法是正交矩阵的行列式是1,正交镜像矩阵的确定行列式是-1。

行列式可以通过 GLSL 函数行列式( mat2( texC_dx, texC_dy ) 计算
或者可以通过公式texC_dx.x * texC_dy.y - texC_dy.x * texC_dx.y计算。

对于正交归一化切空间矩阵的计算,不再需要副法向量和单位向量的计算
可以回避副法向量的归一化(normalize)。

float texDet = texC_dx.x * texC_dy.y - texC_dy.x * texC_dx.y;
vec3 t = texC_dy.y * pos_dx - texC_dx.y * pos_dy;
t      = normalize( t - n * dot( t, n ) );
vec3 b = cross( n, t );                      // b is normlized because n and t are orthonormalized unit vectors
mat3 tbn = mat3( t, sign( texDet ) * b, n ); // take in account the direction of the binormal vector

Based on the answer from kvark, I would like to add more thoughts.

If you are in need of an orthonormalized tangent space matrix you have to do some work any way.
Even if you add tangent and binormal attributes, they will be interpolated during the shader stages
and at the end they are neither normalized nor they are normal to each another.

Let's assume that we have a normalized normalvector n, and we have the tangent t and the binormalb or we can calculate them from the derivations as follows:

// derivations of the fragment position
vec3 pos_dx = dFdx( fragPos );
vec3 pos_dy = dFdy( fragPos );
// derivations of the texture coordinate
vec2 texC_dx = dFdx( texCoord );
vec2 texC_dy = dFdy( texCoord );
// tangent vector and binormal vector
vec3 t = texC_dy.y * pos_dx - texC_dx.y * pos_dy;
vec3 b = texC_dx.x * pos_dy - texC_dy.x * pos_dx;

Of course an orthonormalized tangent space matrix can be calcualted by using the cross product,
but this would only work for right-hand systems. If a matrix was mirrored (left-hand system) it will turn to a right hand system:

t = cross( cross( n, t ), t ); // orthonormalization of the tangent vector
b = cross( n, t );             // orthonormalization of the binormal vector 
                               //   may invert the binormal vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

In the code snippet above the binormal vector is reversed if the tangent space is a left-handed system.
To avoid this, the hard way must be gone:

t = cross( cross( n, t ), t ); // orthonormalization of the tangent vector
b = cross( b, cross( b, n ) ); // orthonormalization of the binormal vectors to the normal vector 
b = cross( cross( t, b ), t ); // orthonormalization of the binormal vectors to the tangent vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

A common way to orthogonalize any matrix is the Gram–Schmidt process:

t = t - n * dot( t, n ); // orthonormalization ot the tangent vectors
b = b - n * dot( b, n ); // orthonormalization of the binormal vectors to the normal vector 
b = b - t * dot( b, t ); // orthonormalization of the binormal vectors to the tangent vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

Another possibility is to use the determinant of the 2*2 matrix, which results from the derivations of the texture coordinates texC_dx, texC_dy, to take the direction of the binormal vector into account. The idea is that the determinant of a orthogonal matrix is 1 and the determined one of a orthogonal mirror matrix -1.

The determinant can eihter be calcualted by the GLSL function determinant( mat2( texC_dx, texC_dy )
or it can be calcualated by it formula texC_dx.x * texC_dy.y - texC_dy.x * texC_dx.y.

For the calculation of the orthonormalized tangent space matrix, the binormal vector is no longer required and the calculation of the unit vector
(normalize) of the binormal vector can be evaded.

float texDet = texC_dx.x * texC_dy.y - texC_dy.x * texC_dx.y;
vec3 t = texC_dy.y * pos_dx - texC_dx.y * pos_dy;
t      = normalize( t - n * dot( t, n ) );
vec3 b = cross( n, t );                      // b is normlized because n and t are orthonormalized unit vectors
mat3 tbn = mat3( t, sign( texDet ) * b, n ); // take in account the direction of the binormal vector
苹果你个爱泡泡 2024-10-28 07:25:28

计算切线的方法有很多种,如果法线贴图烘焙器不采用与渲染器相同的方法,您将得到微妙的伪像。许多面包师使用 MikkTSpace 算法,该算法与片段导数技巧不同。

,如果您有一个使用 MikkTSpace 的程序中的索引网格(并且没有具有相反方向的纹理坐标三角形共享索引),则该算法的困难部分大部分已为您完成,您可以像这样重建切线:

#include <cmath>
#include "glm/geometric.hpp"
#include "glm/vec2.hpp"
#include "glm/vec3.hpp"
#include "glm/vec4.hpp"

using glm::vec2;
using glm::vec3;
using glm::vec4;

void makeTangents(uint32_t nIndices, uint16_t* indices,
                  const vec3 *positions, const vec3 *normals,
                  const vec2 *texCoords, vec4 *tangents) {
  uint32_t inconsistentUvs = 0;
  for (uint32_t l = 0; l < nIndices; ++l) tangents[indices[l]] = vec4(0);
  for (uint32_t l = 0; l < nIndices; ++l) {
    uint32_t i = indices[l];
    uint32_t j = indices[(l + 1) % 3 + l / 3 * 3];
    uint32_t k = indices[(l + 2) % 3 + l / 3 * 3];
    vec3 n = normals[i];
    vec3 v1 = positions[j] - positions[i], v2 = positions[k] - positions[i];
    vec2 t1 = texCoords[j] - texCoords[i], t2 = texCoords[k] - texCoords[i];

    // Is the texture flipped?
    float uv2xArea = t1.x * t2.y - t1.y * t2.x;
    if (std::abs(uv2xArea) < 0x1p-20)
      continue;  // Smaller than 1/2 pixel at 1024x1024
    float flip = uv2xArea > 0 ? 1 : -1;
    // 'flip' or '-flip'; depends on the handedness of the space.
    if (tangents[i].w != 0 && tangents[i].w != -flip) ++inconsistentUvs;
    tangents[i].w = -flip;

    // Project triangle onto tangent plane
    v1 -= n * dot(v1, n);
    v2 -= n * dot(v2, n);
    // Tangent is object space direction of texture coordinates
    vec3 s = normalize((t2.y * v1 - t1.y * v2)*flip);
    
    // Use angle between projected v1 and v2 as weight
    float angle = std::acos(dot(v1, v2) / (length(v1) * length(v2)));
    tangents[i] += vec4(s * angle, 0);
  }
  for (uint32_t l = 0; l < nIndices; ++l) {
    vec4& t = tangents[indices[l]];
    t = vec4(normalize(vec3(t.x, t.y, t.z)), t.w);
  }
  // std::cerr << inconsistentUvs << " inconsistent UVs\n";
}

幸运的是 顶点着色器,它们被旋转到世界空间:

  fragNormal = (model.model * vec4(inNormal, 0)).xyz;
  fragTangent = vec4((model.model * vec4(inTangent.xyz, 0)).xyz, inTangent.w);

然后像这样计算副法线和世界空​​间法线(参见http://mikktspace. com/):(

  vec3 binormal = fragTangent.w * cross(fragNormal, fragTangent.xyz);
  vec3 worldNormal = normalize(normal.x * fragTangent.xyz +
                               normal.y * binormal +
                               normal.z * fragNormal);

副法线通常按像素计算,但有些面包师允许您选择按顶点计算并插值。此页面包含有关特定程序的信息。)

There is a variety of ways to calculate tangents, and if the normal map baker doesn't do it the same way as the renderer you'll get subtle artifacts. Many bakers use the MikkTSpace algorithm, which isn't the same as the fragment derivatives trick.

Fortunately, if you have an indexed mesh from a program that uses MikkTSpace (and no texture coordinate triangles with opposite orientations share an index) the hard part of the algorithm is mostly done for you, and you can reconstruct the tangents like this:

#include <cmath>
#include "glm/geometric.hpp"
#include "glm/vec2.hpp"
#include "glm/vec3.hpp"
#include "glm/vec4.hpp"

using glm::vec2;
using glm::vec3;
using glm::vec4;

void makeTangents(uint32_t nIndices, uint16_t* indices,
                  const vec3 *positions, const vec3 *normals,
                  const vec2 *texCoords, vec4 *tangents) {
  uint32_t inconsistentUvs = 0;
  for (uint32_t l = 0; l < nIndices; ++l) tangents[indices[l]] = vec4(0);
  for (uint32_t l = 0; l < nIndices; ++l) {
    uint32_t i = indices[l];
    uint32_t j = indices[(l + 1) % 3 + l / 3 * 3];
    uint32_t k = indices[(l + 2) % 3 + l / 3 * 3];
    vec3 n = normals[i];
    vec3 v1 = positions[j] - positions[i], v2 = positions[k] - positions[i];
    vec2 t1 = texCoords[j] - texCoords[i], t2 = texCoords[k] - texCoords[i];

    // Is the texture flipped?
    float uv2xArea = t1.x * t2.y - t1.y * t2.x;
    if (std::abs(uv2xArea) < 0x1p-20)
      continue;  // Smaller than 1/2 pixel at 1024x1024
    float flip = uv2xArea > 0 ? 1 : -1;
    // 'flip' or '-flip'; depends on the handedness of the space.
    if (tangents[i].w != 0 && tangents[i].w != -flip) ++inconsistentUvs;
    tangents[i].w = -flip;

    // Project triangle onto tangent plane
    v1 -= n * dot(v1, n);
    v2 -= n * dot(v2, n);
    // Tangent is object space direction of texture coordinates
    vec3 s = normalize((t2.y * v1 - t1.y * v2)*flip);
    
    // Use angle between projected v1 and v2 as weight
    float angle = std::acos(dot(v1, v2) / (length(v1) * length(v2)));
    tangents[i] += vec4(s * angle, 0);
  }
  for (uint32_t l = 0; l < nIndices; ++l) {
    vec4& t = tangents[indices[l]];
    t = vec4(normalize(vec3(t.x, t.y, t.z)), t.w);
  }
  // std::cerr << inconsistentUvs << " inconsistent UVs\n";
}

In the vertex shader, they are rotated into world space:

  fragNormal = (model.model * vec4(inNormal, 0)).xyz;
  fragTangent = vec4((model.model * vec4(inTangent.xyz, 0)).xyz, inTangent.w);

Then the binormal and world space normal are calculated like this (see http://mikktspace.com/):

  vec3 binormal = fragTangent.w * cross(fragNormal, fragTangent.xyz);
  vec3 worldNormal = normalize(normal.x * fragTangent.xyz +
                               normal.y * binormal +
                               normal.z * fragNormal);

(The binormal is usually calculated per pixel, but some bakers give you the option to calculate it per vertex and interpolate it. This page has information about specific programs.)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文