--- title: "纹理" date: 2022-07-09T21:49:09+08:00 --- #### 重心坐标 ###### 重心坐标定义 ![](/../../images/barycentric_coordinates.png) 对于一个三角形的三点坐标A,B,C,平面内的一点(x,y)可以写成三点的线性组合式 $$(x,y)=\alpha A + \beta B + \gamma C = \alpha + \beta + \gamma = 1$$ 此时三个顶点的权重($\alpha , \beta , \gamma$)为(x,y)的重心坐标 注意:如果三个坐标值都为非负,则这个重心位于三角形内部 ###### 三角形的几何角度下求解 ![](../../images/geometric_barycentric.png) 将点与三个顶点相连,三个三角形的面积分别为$A_A,A_B,A_C$对应的重心坐标的计算式, $$\alpha = \frac{A_A}{A_A+A_B+A_C}$$ $$\beta = \frac{A_B}{A_A+A_B+A_C}$$ $$\gamma = \frac{A_C}{A_A+A_B+A_C}$$ 在已知四点坐标的情况下则可通过行列式的几何意义求解(任意两个二维向量组合成矩阵的行列式的绝对值为这两条向量所围成平行四边形的面积) 设任意一点为P(x,y)则 $$A_B = \lvert AP,AC \rvert $$ $$A_C = \lvert AB,AP \rvert $$ $$A_A = \lvert BC,BP \rvert $$ 可得出 $$\gamma = \frac{(y_a - y_b) + (x_b - x_a)y + x_a y_b - x_b y_a }{(y_a - y_b)x_c + (x_b - x_a)y_c + x_a y_b - x_b y_a}$$ $$\beta = \frac{(y_a - y_c)x + (x_c - x_a)y + x_a y_c - x_c y_a}{(y_a - y_c)x_b + (x_c - x_a)y_b + x_a y_c - x_c y_a}$$ $$\alpha = 1 - \beta - \gamma$$ ###### 三角形的坐标系角度下求解 对于重心坐标系存在另一种等价视角: 以A点为原点,AB,AC分别为新的坐标系的单位向量构建坐标系,如图 ![](../../images/barycentric_in_coordinates.jpg) 给定的任意点P的坐标可表示为$P(\beta , \gamma)$ ,可推出P点坐标满足以下关系 $$p = a + \beta(b - a) + \gamma(c - a)$$ 化简后得 $$p = (1 - \beta - \gamma )a + \beta b + \gamma c $$ 表现为一个线性方程组如下 $$ \begin{bmatrix} x_b - x_a & x_c - x_a \\\\ y_b - y_a & y_c - y_a \end{bmatrix} \begin{bmatrix} \beta \\\\ \gamma \end{bmatrix}= \begin{bmatrix} x_p - x_a \\\\ y_p - y_a \end{bmatrix} $$ ###### 重心坐标的用处 对顶点处的值进行线性插值,如下图 ![](../../images/barycentric_coor_interpo.png) $$V = \alpha V_A + \beta V_B + \gamma V_C$$ $V_A,V_B,V_C$ 可以对应为: 1. 位置 2. 纹理坐标 3. 颜色 4. 法线 5. 深度 6. 材质属性 ###### 插值部分代码实现 ``` c++ //三维向量插值 static Eigen::Vector3f interpolate(float alpha, float beta, float gamma, const Eigen::Vector3f& vert1, const Eigen::Vector3f& vert2, const Eigen::Vector3f& vert3, float weight) { return (alpha * vert1 + beta * vert2 + gamma * vert3) / weight; } // 二维向量插值 static Eigen::Vector2f interpolate(float alpha, float beta, float gamma, const Eigen::Vector2f& vert1, const Eigen::Vector2f& vert2, const Eigen::Vector2f& vert3, float weight) { auto u = (alpha * vert1[0] + beta * vert2[0] + gamma * vert3[0]); auto v = (alpha * vert1[1] + beta * vert2[1] + gamma * vert3[1]); u /= weight; v /= weight; return Eigen::Vector2f(u, v); } //计算重心 static std::tuple computeBarycentric2D(float x, float y, const Vector4f* v){ float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y()); float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y()); float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y()); return {c1,c2,c3}; } //光栅化时,判断深度之后进行插值计算得出对应的数值 // auto interpolated_color(颜色) auto interpolated_color=interpolate(alpha,beta,gamma,t.color[0],t.color[1],t.color[2],1); // auto interpolated_normal(法线) auto interpolated_normal = interpolate(alpha,beta,gamma,t.normal[0],t.normal[1],t.normal[2],1).normalized(); // auto interpolated_texcoords(纹理坐标) auto interpolated_texcoords = interpolate(alpha,beta,gamma,t.tex_coords[0],t.tex_coords[1],t.tex_coords[2],1); // auto interpolated_shadingcoords(着色坐标) auto interpolated_shadingcoords = interpolate(alpha,beta,gamma,view_pos[0],view_pos[1],view_pos[2],1); ``` #### 纹理映射处理 ###### 简单的纹理映射 : 漫反射颜色 伪代码 : ``` for each rasterized screen sample (x,y): (u,v) = evaluate texture coordinate at (x,y) texcolor = texture.sample(u,v); set sample's color to texcolor ``` sample(x,y) : 通常为像素的重心 texture coordinate at (x,y):使用重心坐标值 sample's color : 通常为漫反射率值 kd ###### 纹理放大处理(简单方式) 前置知识: 1. 通常不需要进行纹理放大操作,这会导致纹理分辨率不足 2. 纹理的一个像素通常被称为纹理元素或纹素(texel) 假设我们需要在红点处纹理采样的值f(x,y),黑点为对应的纹理采样点 此时我们会选取红点位置周围的四个采样点,同时计算其与周围四个采样点的偏移值(s,t)如下图 ![](../../images/bilinear_interpolation.png) 可得相应的计算过程: 1. 定义线性插值 $$lerp(x,v_0,v_1) = v_0 + x(v_1 - v_0)$$ 2. 计算两个辅助值 $$u_0 = lerp(s,u_{00},u_{10})$$ $$u_1 = lerp(s,u_{01},u_{11})$$ 3. 计算顶点插值得出结果 $$f(x,y) = lerp(t,u_0,u_1)$$ 采用线性插值的方式进行纹理放大的计算往往能够获得很好的结果和平衡的性能消耗 ###### 纹理放大至过大的情况 此时在点采样下会产生摩尔纹和锯齿,如下图 ![](../../images/moire_and_jaggies.png) 通常处理是进行超采样,同时也会有几个问题 1. 超采样能产生质量足够高的结果,带来的性能开销也很高 2. 当纹理极度缩小时,许多纹素会被压缩在内存(texel footprint)里 3. 单一像素中的信号序列过大 4. 需要更高的采样频率 所以我们更倾向于使用范围序列而非点序列 ![](../../images/point_query_and_range_query.png) ###### Mipmap(支持 Range Query) mipmap 可由下图表示 ![](../../images/mipmap_information.png) 对应产生的mip层次结构也由下图表示 ![](../../images/mip_hierarchy.png) 在计算对应层次的mipmap时,我们需要相邻采样点的纹理坐标来估计纹理内存 ![](../../images/compute_mip_level_D.png) 如上图所示,我们能够得到两个值 $$L = \max \left( \sqrt{(\frac{d_u}{d_X})^2 + (\frac{d_v}{d_X})^2},\sqrt{(\frac{d_u}{d_y})^2 + (\frac{d_v}{d_y})^2} \right) $$ $$D = \log_2 L$$ 在使用时往往D值会四舍五入为整数,这会导致纹理的不连续,因此,我们还需要进行下图所示的Trilinear Interpolation 从而得到一个连续的D值 ![](../../images/Trilinear_interpolation.png) 在插值处理后的纹理依旧会存在过度模糊(overblur)的现象,但相比于超采样的高性能开销依旧有很大的提升 ###### Anisotropic Filtering and EWA filtering Anisotropic Filtering 1. 可以查找轴对齐的矩形区域纹理 2. 对角线纹理处理依旧存在问题 ![](../../images/Anisotropic_filtering.png) EWA filtering 1. 权重均衡 2. 采用mipmap层次结构 3. 可以处理纹理中的不规则像素(irregular footprints) Irregular Pixel Footprint in Texture的产生情况如下图 ![](../../images/irregular_pixel_footprint.png) #### 纹理的应用 ##### 纹理的用途 在现代GPU中,纹理贴图 = 内存数据 + 范围序列(reang query)即反走样 常用的方式是向片元计算中提供相应的数据 应用的地方: 1. 环境光 2. 存储微表面 3. 程序化纹理生成 4. 实体建模 5. 立体渲染(体绘制) ###### 环境光贴图和立方体贴图(Environment and Cube Map) 环境光贴图通常指在贴图上绘制模型周围环境光效,实现某些模型反射环境光的效果 ![](../../images/Environment_Map.png) 也用于一些镜面反射的物体实现真实光照 ![](../../images/Environmental_Lighting.png) ![](../../images/Spherical_Environment_Map.png) 在一些球型表面贴图会存在形变问题,如下图的顶部和底部 ![](../../images/Spherical_Map_Problem.png) 立方体贴图则由球体上一个向量沿自身方向映射到立方体上的点获得 ![](../../images/Cube_Map.png) 一个立方体使用6个方形纹理贴图来生成纹理 ###### Bump Mapping 在不提高模型面数的情况下展示表面细节 1. 对表面的每一条法线进行扰动(仅用于着色计算) 2. 高度偏移纹理定义的每个纹素 扰动法线的计算(平面) 1. 假设原平面法线为$$n(p) = (0,1)$$ 2. $ p $点处的导数为 $$dp = c * [h(p + 1) - h(p)]$$ 3. 扰动后的法线值为$$n(p) = (-dp,1).normalized()$$ ![](../../images/perturb_the_normal.png) 扰动法线的计算(3D) 1. 假设原平面法线为$$n(p) = (0,0,1)$$ 2. $p$点处的导数为 $$\frac{d_p}{d_u} = c_1 * [h(u + 1) - h(u)]$$ $$\frac{d_p}{d_v} = c_2 * [h(v + 1) - h(v)]$$ 3. 扰动后的法线为$$n = (\frac{-d_p}{d_u},\frac{-d_p}{d_v},1).normalized()$$ 4. 坐标计算基于本地坐标系 部份代码实现 ```c++ float x = normal.x(); float y = normal.y(); float z = normal.z(); Eigen::Vector3f t = Eigen::Vector3f(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z)); Eigen::Vector3f b = normal.cross(t); Eigen::Matrix3f TBN; TBN << t.x(),b.y(),normal.x(), t.y(),b.x(),normal.y(), t.z(),b.z(),normal.z(); float u = payload.tex_coords.x(); float v = payload.tex_coords.y(); float w = payload.texture->width; float h = payload.texture->height; float dU=kh * kn * (payload.texture->getColor(u+1.0f/w,v).norm()-payload.texture->getColor(u,v).norm()); float dV=kh * kn * (payload.texture->getColor(u,v+1.0f/h).norm()-payload.texture->getColor(u,v).norm()); Eigen::Vector3f ln = Eigen::Vector3f(-dU,-dV,1.0f); normal=TBN *ln; // Let n = normal = (x, y, z) // Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z)) // Vector b = n cross product t // Matrix TBN = [t b n] // dU = kh * kn * (h(u+1/w,v)-h(u,v)) // dV = kh * kn * (h(u,v+1/h)-h(u,v)) // Vector ln = (-dU, -dV, 1) // Normal n = normalize(TBN * ln) Eigen::Vector3f result_color = {0, 0, 0}; result_color = normal; ``` ###### 纹理贴图能够影响着色效果 Displacement mapping 1. 是Bump mapping的更进一步 2. 与Bump mapping 使用相同纹理贴图 3. 会改变模型顶点坐标 ![](../../images/Displacement_mapping.png) 3D程序化噪声和实体建模 ![](../../images/Procedural_noise_and_Solid.png) 将部分着色效果提前绘制到贴图上 ![](../../images/Provide_precomputed_shading.png) 3D图像纹理和体绘制 ![](../../images/3D_texture_and_volume.png)