前言

在很早之前的纹理映射中,纹理存放的元素是像素的颜色,通过纹理坐标映射到目标像素以获取其颜色。但是我们的法向量依然只是定义在顶点上,对于三角形面内一点的法向量,也只是通过比较简单的插值法计算出相应的法向量值。这对平整的表面比较有用,但无法表现出内部粗糙的表面。在这一章,你将了解如何获取更高精度的法向量以描述一个粗糙平面。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

法线贴图

法线贴图是指纹理中实际存放的元素通常是经过压缩后的法向量,用于表现一个表面凹凸不平的特性,它是凹凸贴图的一种实现方式。

开启法线贴图后的效果



关闭法线贴图后的效果



法线贴图中存放的法向量\((x, y, z)\)分别对应原来的\((r, g, b)\)。每个像素都存放了对应的一个法向量,经过压缩后使用24 bit即可表示。实际情况则是一张法线贴图里面的每个像素使用了32 bit来表示,剩余的8 bit(位于Alpha值)要么可以不使用,要么用来表示高度值或者镜面系数。而未经压缩的法线贴图通常为每个像素存放4个浮点数,即使用128 bit来表示。

下面展示了一张法线贴图,每个像素点位置存放了任意方向的法向量。可以看到这里为法线贴图建立了一个TBN坐标系(左手坐标系),其中T轴(Tangent Axis)对应原来的x轴,B轴(Binormal Axis)对应原来的y轴,N轴(Normal Axis)对应原来的z轴。建立坐标系的目的在后面再详细描述。观察这些法向量,它们都有一个共同的特点,就是都朝着N轴的正方向散射,这样使得大多数法向量的z分量是最大的。

由于压缩后的法线贴图通常是以R8G8B8A8的格式存储,我们也可以直接把它当做图片来打开观察。

前面说到大部分法向量的z分量会比x, y分量大,导致整个图看起来会偏蓝。

法线贴图的压缩与解压

经过初步压缩后的法线贴图的占用空间为原来的1/4(不考虑文件头),就算每个分量只有256种表示,也足够表示出16777216种不同的法向量了。假如现在我们已经有未经过压缩的法线贴图,那要怎么进行初步压缩呢?

对于一个单位法向量来说,其任意一个分量的取值也无非就是落在[-1, 1]的区间上。现在我们要将其映射到[0, 255]的区间上,可以用下面的公式来进行压缩:

\[f(x) = (0.5x + 0.5) * 255
\]

而如果现在拿到的是24位法向量,要进行还原,则可以用下面的公式:

\[f^-1(x) = \frac{2x}{255} - 1
\]

当然,经过还原后的法向量是有部分的精度损失了,至少能够映射回[-1, 1]的区间上。

通常情况下我们能拿到的都是经过压缩后的法线贴图,但是还原工作还是需要由自己来完成。

float3 normalT = gNormalMap.Sample(sam, pin.Tex);

经过上面的采样后,normalT的每个分量会自动从[0, 255]映射到[0, 1],但还不是最终[-1, 1]的区间。因此我们还需要完成下面这一步:

normalT = 2.0f * normalT - 1.0f;

这里的1.0f会扩展成float3(1.0f, 1.0f, 1.0f)以完成减法运算。

注意:如果你想要使用压缩纹理格式(对原来的R8G8B8A8进一步压缩)来存储法线贴图,可以使用BC7(DXGI_FORMAT_BC7_UNORM)来获得最佳性能。在DirectXTex中有大量从BC1到BC7的纹理压缩/解压函数。

纹理/切线空间

这里开始就会产生一个疑问了,为什么需要切线空间?

在只有2D的纹理坐标系仅包含了U轴和V轴,但现在我们的纹理中存放的是法向量,这些法向量要怎么变换到局部物体上某一个三角形对应位置呢?这就需要我们对当前法向量做一次矩阵变换(平移和旋转),使它能够来到局部坐标系下物体的某处表面。由于矩阵变换涉及到的是坐标系变换,我们需要先在原来的2D纹理坐标系加一条坐标轴(N轴),与T轴(原来的U轴)和B轴(原来的V轴)相互垂直,以此形成切线空间。

一开始法向量处在单位切线空间,而需要变换到目标3D三角形的位置也有一个对应的切线空间。对于一个立方体来说,一个面的两个三角形可以共用一个切线空间。

利用顶点位置和纹理坐标求TBN坐标系

现在假设我们的顶点只包含了位置和纹理坐标这两个信息,有这样一个三角形,它们的顶点为V0(x0, y0, z0), V1(x1, y1, z1), V2(x2, y2, z2),纹理坐标为(u0, v0), (u1, v1), (u2, v2)。

图片展示了一个三角形与所处的切线空间,我们可以这样定义向量E0E1

\[\mathbf{e_0} = \mathbf{V_1} - \mathbf{V_0} \\
\mathbf{e_1} = \mathbf{V_2} - \mathbf{V_0}
\]

现在T轴和B轴都是待求的单位向量,可以列出下述关系:

\[(\Delta u_0, \Delta v_0) = (u_1 - u_0, v_1 - v_0) \\
(\Delta u_1, \Delta v_1) = (u_2 - u_0, v_2 - v_0) \\
\mathbf{e_0} = \Delta u_0\mathbf{T} + \Delta v_0\mathbf{B} \\
\mathbf{e_1} = \Delta u_1\mathbf{T} + \Delta v_1\mathbf{B}
\]

把它用矩阵来描述:

\[\begin{bmatrix} \mathbf{e_0} \\ \mathbf{e_1} \end{bmatrix} =
\begin{bmatrix} \Delta u_0 & \Delta v_0 \\ \Delta u_1 & \Delta v_1 \end{bmatrix}
\begin{bmatrix} \mathbf{T} \\ \mathbf{B} \end{bmatrix}
\]

继续细化:

\[\begin{bmatrix} e_{0x} & e_{0y} & e_{0z} \\ e_{1x} & e_{1y} & e_{1z} \end{bmatrix} =
\begin{bmatrix} \Delta u_0 & \Delta v_0 \\ \Delta u_1 & \Delta v_1 \end{bmatrix}
\begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \end{bmatrix}
\]

为了计算TB矩阵,需要在等式两边左乘uv矩阵的逆:

\[{\begin{bmatrix} \Delta u_0 & \Delta v_0 \\ \Delta u_1 & \Delta v_1 \end{bmatrix}}^{-1}
\begin{bmatrix} e_{0x} & e_{0y} & e_{0z} \\ e_{1x} & e_{1y} & e_{1z} \end{bmatrix} =
\begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \end{bmatrix}
\]

对于一个二阶矩阵顶点求逆,我们不考虑过程。已知有矩阵\(\mathbf{A} = \begin{bmatrix} a & b \\ c & d \end{bmatrix}\),那么它的逆矩阵为:

\[\mathbf{A}^{-1} = \frac{1}{ad-bc}\begin{bmatrix} d & -b \\ -c & a \end{bmatrix}
\]

因此上面的方程最终变成:

\[\begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \end{bmatrix} =
\frac{1}{\Delta u_0 \Delta v_1 - \Delta v_0 \Delta u_1}
\begin{bmatrix} \Delta v_1 & - \Delta v_0 \\ -\Delta u_1 & \Delta u_0 \end{bmatrix}
\begin{bmatrix} e_{0x} & e_{0y} & e_{0z} \\ e_{1x} & e_{1y} & e_{1z} \end{bmatrix}
\]

这里可以找一个例子尝试一下:

V0坐标为(0, 0, -0.25), 纹理坐标为(0, 0.5)

V1坐标为(0.15, 0, 0), 纹理坐标为(0.3, 0)

V2坐标为(0.4, 0, 0), 纹理坐标为(0.8, 0)

求解过程如下:

\[\begin{align}\mathbf{e_0} &= \mathbf{V_1} - \mathbf{V_0} = (0.15, 0, 0.25) \\
\mathbf{e_1} &= \mathbf{V_2} - \mathbf{V_0} = (0.4, 0, 0.25) \\
(\Delta u_0, \Delta v_0) &= (u_1 - u_0, v_1 - v_0) = (0.3, -0.5) \\
(\Delta u_1, \Delta v_1) &= (u_2 - u_0, v_2 - v_0) = (0.8, -0.5) \\
\begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \end{bmatrix} &=
\frac{1}{0.3 \times (-0.5) - (-0.5) \times 0.8}
\begin{bmatrix} -0.5 & 0.5 \\ -0.8 & 0.3 \end{bmatrix}
\begin{bmatrix} 0.15 & 0 & 0.25 \\ 0.4 & 0 & 0.25 \end{bmatrix} =
\begin{bmatrix} 0.5 & 0 & 0 \\ 0 & 0 & -0.5 \end{bmatrix}\end{align}
\]

由于位置坐标和纹理坐标的不一致性,导致求出来的T向量和B向量很有可能不是单位向量。仅当位置坐标的变化率与纹理坐标的变化率相同时才会得到单位向量。这里我们将其进行标准化即可。

但如果对纹理坐标进行了变换,有可能导致T轴和B轴不相互垂直。比如尝试用球体网格模型某个三角形面内的一点映射到球面上一点。

顶点切线空间

上面的运算得到的切线空间是基于单个三角形的,可以看到其运算过程还是比较复杂,而且交给着色器来进行运算的话还会产生大量的指令。

我们可以为顶点添加法向量N和切线向量T用于构建基于顶点的切线空间。很早之前提到法向量是与该顶点共用的所有三角形的法向量取平均值所得到的。切线向量也一样,它是与该顶点共用的所有三角形的切线向量取平均值所得到的。

现在Vertex.h定义了我们的新顶点类型:

struct VertexPosNormalTangentTex
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT4 tangent;
DirectX::XMFLOAT2 tex;
static const D3D11_INPUT_ELEMENT_DESC inputLayout[4];
};

这里的tangent是一个4D向量,考虑到要和微软DXTK定义的顶点类型保持一致,多出来的w分量可以留作他用,这里暂不讨论。

施密特向量正交化

通常顶点提供的NT通常是相互垂直的,并且都是单位向量,我们可以通过计算\(\mathbf{B} = \mathbf{N} \times \mathbf{T}\)来得到副法线向量B,使得顶点可以不需要存放副法线向量B。但是经过插值计算后的NT可能会导致不是相互垂直,我们最好还是要通过施密特正交化来获得实际的切线空间。

现在已知互不垂直的N向量和T向量,我们希望求出与N向量垂直的T'向量,需要将T向量投影到N向量上。

从上面的图我们可以知道最终求得的T'

\[\mathbf{T'} = \lVert \mathbf{T} - (\mathbf{T} \cdot \mathbf{N}) \mathbf{N} \rVert
\]

B' 最终也可以确定下来

\[\mathbf{B'} = \mathbf{N} \times \mathbf{T'}
\]

这样T', B', N相互垂直,可以构成TBN坐标系。在后面的着色器实现中我们也会用到这部分内容。

切线空间的变换

一开始的切线空间可以用一个单位矩阵来表示,切线向量正是处在这个空间中。紧接着就是需要对其进行一次到局部对象(具体到某个三角形)切线空间的变换:

\[\mathbf{M}_{object} = \begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \\ N_x & N_y & N_z \end{bmatrix}
\]

然后切线向量随同世界矩阵一同进行变换来到世界坐标系,因此我们可以把它写成:

\[\mathbf{n}_{world} = \mathbf{n}_{tangent}\mathbf{M}_{object}\mathbf{M}_{world}
\]

注意:

  1. 对切线向量进行矩阵变换,我们只需要使用3x3的矩阵即可。
  2. 法线向量变换到世界矩阵需要用世界矩阵求逆的转置进行校正,而对切线向量只需要用世界矩阵变换即可。下图演示了将宽度拉伸为原来2倍后,法线和切线向量的变化:

HLSL代码

为了使用法线贴图,我们需要完成下列步骤:

  1. 获取该纹理所需要用到的法线贴图,在C++端为其创建一个ID3D11Texture2D。这里不考虑如何制作一张法线贴图。
  2. 对于一个网格模型来说,顶点数据需要包含位置、法向量、切线向量、纹理坐标四个元素。同样这里不讨论模型的制作,在本教程使用的是Geometry所生成的网格模型
  3. 在顶点着色器中,将顶点法向量和切线向量从局部坐标系变换到世界坐标系
  4. 在像素着色器中,使用经过插值的法向量和切线向量来为每个三角形表面的像素点构建TBN坐标系,然后将切线空间的法向量变换到世界坐标系中,这样最终求得的法向量用于光照计算。

现在我们的Basic.hlsli沿用的是第23章动态天空盒的部分,变化如下:

Texture2D g_DiffuseMap : register(t0);
Texture2D g_NormalMap : register(t1);
TextureCube g_TexCube : register(t2);
SamplerState g_Sam : register(s0); // 使用的是第23章的常量缓冲区,省略...
// 省略和之前一样的结构体... struct VertexPosNormalTangentTex
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float4 TangentL : TANGENT;
float2 Tex : TEXCOORD;
}; struct InstancePosNormalTangentTex
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float4 TangentL : TANGENT;
float2 Tex : TEXCOORD;
matrix World : World;
matrix WorldInvTranspose : WorldInvTranspose;
}; struct VertexPosHWNormalTangentTex
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION; // 在世界中的位置
float3 NormalW : NORMAL; // 法向量在世界中的方向
float4 TangentW : TANGENT; // 切线在世界中的方向
float2 Tex : TEXCOORD;
}; float3 NormalSampleToWorldSpace(float3 normalMapSample,
float3 unitNormalW,
float4 tangentW)
{
// 将读取到法向量中的每个分量从[0, 1]还原到[-1, 1]
float3 normalT = 2.0f * normalMapSample - 1.0f; // 构建位于世界坐标系的切线空间
float3 N = unitNormalW;
float3 T = normalize(tangentW.xyz - dot(tangentW.xyz, N) * N); // 施密特正交化
float3 B = cross(N, T); float3x3 TBN = float3x3(T, B, N); // 将凹凸法向量从切线空间变换到世界坐标系
float3 bumpedNormalW = mul(normalT, TBN); return bumpedNormalW;
}

上面的NormalSampleToWorldSpace函数用于将法向量从切线空间变换到世界空间,位于Basic.hlsli。它接受了3个参数:从法线贴图采样得到的向量,变换到世界坐标系的法向量和切线向量。

然后是顶点着色器:

// NormalMapObject_VS.hlsl
#include "Basic.hlsli" // 顶点着色器
VertexPosHWNormalTangentTex VS(VertexPosNormalTangentTex vIn)
{
VertexPosHWNormalTangentTex vOut; matrix viewProj = mul(g_View, g_Proj);
vector posW = mul(float4(vIn.PosL, 1.0f), g_World); vOut.PosW = posW.xyz;
vOut.PosH = mul(posW, viewProj);
vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
vOut.TangentW = mul(vIn.TangentL, g_World);
vOut.Tex = vIn.Tex;
return vOut;
}
// NormalMapInstance_VS.hlsl
#include "Basic.hlsli" // 顶点着色器
VertexPosHWNormalTangentTex VS(InstancePosNormalTangentTex vIn)
{
VertexPosHWNormalTangentTex vOut; matrix viewProj = mul(g_View, g_Proj);
vector posW = mul(float4(vIn.PosL, 1.0f), vIn.World); vOut.PosW = posW.xyz;
vOut.PosH = mul(posW, viewProj);
vOut.NormalW = mul(vIn.NormalL, (float3x3) vIn.WorldInvTranspose);
vOut.TangentW = mul(vIn.TangentL, vIn.World);
vOut.Tex = vIn.Tex;
return vOut;
}

相比之前的像素着色器,现在它多了对法线映射的处理:

// 法线映射
float3 normalMapSample = g_NormalMap.Sample(g_Sam, pIn.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(normalMapSample, pIn.NormalW, pIn.TangentW);

求得的法向量bumpedNormalW将用于光照计算。

现在完整的像素着色器代码如下:

// NormalMap_PS.hlsl
#include "Basic.hlsli" // 像素着色器(3D)
float4 PS(VertexPosHWNormalTangentTex pIn) : SV_Target
{
// 若不使用纹理,则使用默认白色
float4 texColor = float4(1.0f, 1.0f, 1.0f, 1.0f); if (g_TextureUsed)
{
texColor = g_DiffuseMap.Sample(g_Sam, pIn.Tex);
// 提前进行裁剪,对不符合要求的像素可以避免后续运算
clip(texColor.a - 0.1f);
} // 标准化法向量
pIn.NormalW = normalize(pIn.NormalW); // 求出顶点指向眼睛的向量,以及顶点与眼睛的距离
float3 toEyeW = normalize(g_EyePosW - pIn.PosW);
float distToEye = distance(g_EyePosW, pIn.PosW); // 法线映射
float3 normalMapSample = g_NormalMap.Sample(g_Sam, pIn.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(normalMapSample, pIn.NormalW, pIn.TangentW); // 初始化为0
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);
int i; [unroll]
for (i = 0; i < 5; ++i)
{
ComputeDirectionalLight(g_Material, g_DirLight[i], bumpedNormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
} [unroll]
for (i = 0; i < 5; ++i)
{
ComputePointLight(g_Material, g_PointLight[i], pIn.PosW, bumpedNormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
} [unroll]
for (i = 0; i < 5; ++i)
{
ComputeSpotLight(g_Material, g_SpotLight[i], pIn.PosW, bumpedNormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
} float4 litColor = texColor * (ambient + diffuse) + spec; // 反射
if (g_ReflectionEnabled)
{
float3 incident = -toEyeW;
float3 reflectionVector = reflect(incident, pIn.NormalW);
float4 reflectionColor = g_TexCube.Sample(g_Sam, reflectionVector); litColor += g_Material.Reflect * reflectionColor;
}
// 折射
if (g_RefractionEnabled)
{
float3 incident = -toEyeW;
float3 refractionVector = refract(incident, pIn.NormalW, g_Eta);
float4 refractionColor = g_TexCube.Sample(g_Sam, refractionVector); litColor += g_Material.Reflect * refractionColor;
} litColor.a = texColor.a * g_Material.Diffuse.a;
return litColor;
}

所有的着色器将共用Basic.hlsli。而对BasicEffect的变化(和C++的交互)这里我们不讨论。

下面的动画演示了法线贴图的对比效果(GIF画质有点渣):

至此进阶篇就告一段落了。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

DirectX11 With Windows SDK--25 法线贴图的更多相关文章

  1. 粒子系统与雨的效果 (DirectX11 with Windows SDK)

    前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...

  2. DirectX11 With Windows SDK--34 位移贴图

    前言 在前面的章节中,我们学到了法线贴图和曲面细分.现在我们可以将这两者进行结合以改善效果,因为法线贴图仅仅只是改善了光照的细节,但它并没有从根本上改善几何体的细节.从某种意义上来说,法线贴图只是一个 ...

  3. DirectX11 With Windows SDK--00 目录

    前言 (更新于 2019/4/10) 从第一次接触DirectX 11到现在已经有将近两年的时间了.还记得前年暑假被要求学习DirectX 11,在用龙书的源码配置项目运行环境的时候都花了好几天的时间 ...

  4. DirectX11 With Windows SDK--07 添加光照与常用几何模型

    前言 对于3D游戏来说,合理的光照可以让游戏显得更加真实.接下来会介绍光照的各种分量,以及常见的光照模型.除此之外,该项目还用到了多个常量缓冲区,因此还会提及HLSL的常量缓冲区打包规则以及如何设置多 ...

  5. DirectX11 With Windows SDK--09 纹理映射与采样器状态

    前言 在之前的DirectX SDK中,纹理的读取使用的是D3DX11CreateShaderResourceViewFromFile函数,现在在Windows SDK中已经没有这些函数,我们需要找到 ...

  6. DirectX11 With Windows SDK--32 SSAO(屏幕空间环境光遮蔽)

    前言 由于性能的限制,实时光照模型往往会忽略间接光因素(即场景中其他物体所反弹的光线).但在现实生活中,大部分光照其实是间接光.在第7章里面的光照方程里面引入了环境光项: \[C_a = \mathb ...

  7. DirectX11 With Windows SDK--33 曲面细分阶段(Tessellation)

    前言 曲面细分是Direct3D 11带来的其中一项重要的新功能.它引入了两个可编程着色器阶段以及一个固定的镶嵌处理过程.简单来说,曲面细分技术可以将几何体细分为更小的三角形,并以某种方式把这些新生成 ...

  8. DirectX11 With Windows SDK--36 延迟渲染基础

    前言 随着图形硬件变得越来越通用和可编程化,采用实时3D图形渲染的应用程序已经开始探索传统渲染管线的替代方案,以避免其缺点.其中一项最流行的技术就是所谓的延迟渲染.这项技术主要是为了支持大量的动态灯光 ...

  9. DirectX11 With Windows SDK--38 级联阴影映射(CSM)

    前言 在31章我们曾经实现过阴影映射,但是受到阴影贴图精度的限制,只能在场景中相当有限的范围内投射阴影.本章我们将以微软提供的例子和博客作为切入点,学习如何解决阴影中出现的Atrifacts: 边缘闪 ...

随机推荐

  1. RPM-GPG-KEY详解

    GPG在Linux上的应用主要是实现官方发布的包的签名机制 GPG分为公钥及私钥 公钥:顾名思意,即可共享的密钥,主要用于验证私钥加密的数据及签名要发送给私钥方的数据 私钥:由本地保留的密钥,用于签名 ...

  2. Map根据value值进行倒序排列

    public List<Map.Entry<Integer,BigDecimal>> sortByMapValue(HashMap<Integer, BigDecimal ...

  3. asp.net core 排序过滤分页组件:sieve(2)表达式树的复习

    在Sieve组件中使用了很多关于表达式树的知识,但在我们日常的工作中写表达式树的机会是非常少的,至少在我的编程生涯中没怎么写过表达式树(可能也就是3,4次).所以,为了能够看懂Sieve里面的源代码, ...

  4. DDctf 新得

    滴这道题当时做的时候只做到了看到index.php的源码 当时给了一个博客的提示猜到是swp的那个 但是没有想到是里面的 文件就没有做了,然后在看了wp过后就明白了 访问博客我文章里面的文章里面的pr ...

  5. MongoDB索引基本操作

    一.简介 在MongoDB建立索引能提高查询效率,只需要扫描索引只存储的这个集合的一小部分,并只把这小部分加载到内存中,效率大大的提高,如果没有建立索引,在查询时,MongoDB必须执行全表扫描,在数 ...

  6. FreeHttp (a fiddler add in to temper the http)

    introduction FreeHttp is a Fiddler plugin. With FreeHttp you can modify the request or response mess ...

  7. 一些矩阵范数的subgradients

    目录 引 正交不变范数 定理1 定理2 例子:谱范数 例子:核范数 算子范数 定理3 定理4 例子 \(\ell_2\) <Subgradients> Subderivate-wiki S ...

  8. django开发新手教程(原创)

    为了帮助新手简单高效解决django开发的问题,从而写了这么一篇,随便转载! 本人用的是windows10操作系统 #联网安装 ==指定版本号      我在自定义的www文件夹安装C:\Users\ ...

  9. springboot开启事务支持时报代理错误

    问题:The bean 'xxx' could not be injected as a 'com.github.service.xx' because it is a JDK dynamic pro ...

  10. Asp.Net Core Options模式的知识总结

    Options模式是Asp.Net Core中用于配置的一种模式,它利用了系统的依赖注入,并且还可以利用配置系统.它使我们可以采用依赖注入的方法直接使用绑定的一个POCO对象,这个POCO对象就叫做O ...