CSharpGL(48)用ShadowVolume画模型的影子
CSharpGL(48)用ShadowVolume画模型的影子
Shadow Volume
在Per-Fragment Operations & Tests阶段,有一个步骤是模版测试(Stencil Test)。依靠这一步骤,不仅可以实现渲染模型的包围框这样的实用功能,还能创造出一种渲染阴影的算法,即Shadow Volume算法。
用Shadow Mapping方法得到的阴影,在贴近观察时,会看到细微的锯齿。这是因为深度缓存受到分辨率的限制,不可能完全精确地描述贴近观察时的各个Fragment。但Shadow Volume方法得到的阴影是没有这样的锯齿问题的,如下图所示:
对比(左)Shadow Mapping的阴影有锯齿(右)Shadow Volume的阴影更平滑
如图所示,左侧的Shadow Mapping方法得到的阴影在犄角、舌头和下巴部分可以看到比较明显的锯齿,而右侧的Shadow Volume方法得到的阴影则十分平滑。这是由Shadow Volume的实现机制决定的。其机制概括起来就是,根据光源位置(或方向)和模型位置,动态地生成一个不规则的包围盒,将阴影部分包裹起来,保证包围盒内部的Fragment不参与光照计算。上图的包围盒如下图所示:
从6个视角观察包围盒
注意,这个包围盒是实时动态生成的,它会随着光源位置(或方向)的变化而变化。而且,这个包围盒是延伸到无限远的。这样才能正确地渲染出阴影。
那么有2个主要问题:首先,如何动态生成这个包围盒,而且能够覆盖无限远的范围;然后如何根据包围盒判断Fragment是否参与光照计算。
为便于理解问题,这里假设探讨的模型都是由三角形网格拼接组成的。例如对于中国龙模型和Cube模型,其三角形网格结构如下图所示:
三角形网格组成的模型
可以看到,中国龙模型是由非常多的三角形网格拼接而成的,这利于观察光照效果的真实感。Cube模型则仅仅由12个三角形拼接而成,这利于检测程序的正确性。请读者在随书代码中找到任意一个可以渲染中国龙模型的项目,为其添加PolygonModeSwitch开关,近距离观察中国龙模型的三角形网格。
包围盒
要找到一个模型在光源照射下的包围盒,首先要找到在光源照射下的外围轮廓(Outline)。轮廓的一侧都是能被光源照射到的三角形,另一侧都是不能被光源照射到的三角形(即处于阴影中),例如下图所示:
(左)模型+轮廓线(中)模型(右)轮廓线
在此场景中,在中国龙模型的头部方向上有一个聚光灯光源(下方的Cube模型也是)。图左的白线勾勒出此时的轮廓线在模型上的位置,图中为模型本身,图右为隐藏了模型的轮廓线全貌。
轮廓是由一条条线段组成的。对于组成线段的每个顶点,沿着从光源位置到顶点的方向,无限地延伸出去,就是要找的包围盒的侧面。然后再把轮廓中朝向光源的一侧加上,再把无限远处封口,就得到了一个完整的包围盒。完整的包围盒如下图所示:
从远到近观察完整的包围盒
如图所示,完整的包围盒从模型的位置,无限地延伸到了场景的边缘。图左为地面遮挡了一部分包围盒的情形,图右为隐藏了地面后显示出的更完整的包围盒。可以看到包围盒在最远处仍然呈现出模型的轮廓的形状,且包围盒的近端和远端保持着对应关系。
注意,包围盒是一个完全封闭的盒子,其法线全部指向外侧。这是在构造包围盒时特意设计的。这样才能在后续的判断过程中找到阴影。
动态生成包围盒
第一个问题,如何判断哪个顶点是在轮廓线上的呢?观察下图:
光线L照射到2个三角形上
如图所示,两个三角形的交界处,是一条共享的线段AB。三角形ABC正面向光源L,能够被照射到,另一个三角形ABD则背面向光源L,即处于阴影中。那么这条共享的线段AB就应当成为轮廓线的一部分。如果两个三角形同时正面向光源或同时背面向光源,那么它们之间的共享线段就不需要算到轮廓线里。要判断一个三角形是否正面朝向光源,只需将光源的方向向量L与三角形面的法线向量N做dot乘法,如果结果为负数,说明是正面朝向光源;如果结果为正数,说明是反面朝向光源。
注意,这里使用的是三角形面的法线向量。Vertex Shader只能处理单个的顶点。要处理三角形面这样的对象,就要使用Geometry Shader。为了得知一个三角形与周围哪些三角形有共享边,这里需要使用GL_TRIANGLES_ADJACENCY模式渲染的模型。这样的模型有一个特点,即每个三角形都包含了其周边三角形的信息,如下图所示:
GL_TRIANGLES_ADJACENCY模式的图元
此图展示了一个三角形网格模型的一部分(由6个顶点组成的4个三角形)。其中三角形ACE是Geometry Shader要处理的一个三角形,而三角形ABC、CDE和EFA是与三角形ACE有共享边的三个三角形。这三个三角形被称为三角形ACE的邻接三角形。向Geometry Shader依次传入这6个顶点,即可找到哪些边构成了模型的轮廓线。一般的,模型数据中是不包含邻接信息的,这需要在加载模型后额外计算。
为实现Shadow Volume算法,找轮廓线的任务就是在Geometry Shader中完成的,其代码如下:
#version layout (triangles_adjacency) in; // six vertices in
layout (line_strip, max_vertices = ) out; in vec3 PosL[]; // an array of 6 vertices (triangle with adjacency) uniform vec3 gLightPos; // point light's position.
uniform mat4 gProjectionView;
uniform mat4 gWorld; // Emit a line using a line strip
void EmitOutline(vec3 StartVertex, vec3 EndVertex)
{
gl_Position = gProjectionView * vec4(StartVertex, 1.0);
EmitVertex(); // first vertex of the line. gl_Position = gProjectionView * vec4(EndVertex, 1.0);
EmitVertex(); // second vertex of the line. EndPrimitive(); // the line is complete.
} void main()
{
vec3 worldPos[]; // vertexes’ positions in world space.
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
// edges.
vec3 e1 = worldPos[] - worldPos[];
vec3 e2 = worldPos[] - worldPos[];
vec3 e3 = worldPos[] - worldPos[];
vec3 e4 = worldPos[] - worldPos[];
vec3 e5 = worldPos[] - worldPos[];
vec3 e6 = worldPos[] - worldPos[]; vec3 Normal = normalize(cross(e1,e2)); // normal of triangle face.
vec3 LightDir = normalize(gLightPos - worldPos[]); // vertex to light’s direction. // Handle only light facing triangles
if (dot(Normal, LightDir) > ) {
Normal = cross(e3,e1);
if (dot(Normal, LightDir) <= ) {
vec3 StartVertex = worldPos[];
vec3 EndVertex = worldPos[];
EmitOutline(StartVertex, EndVertex);
} Normal = cross(e4,e5);
LightDir = normalize(gLightPos - worldPos[]);
if (dot(Normal, LightDir) <= ) {
vec3 StartVertex = worldPos[];
vec3 EndVertex = worldPos[];
EmitOutline(StartVertex, EndVertex);
} Normal = cross(e2,e6);
LightDir = normalize(gLightPos - worldPos[]);
if (dot(Normal, LightDir) <= ) {
vec3 StartVertex = worldPos[];
vec3 EndVertex = worldPos[];
EmitOutline(StartVertex, EndVertex);
}
}
}
EmitOutline
注意,代码中的lightDir变量指的是从顶点到光源位置的向量,而图示 7‑13中的光源方向向量L是从光源到顶点的向量。两者是相反的。因此在代码中正面朝向光源的三角形面的法向量N与lightDir的dot结果为正数。
第二个问题,有了轮廓线,将轮廓线的每一条线段都延伸出去,分别形成一个四边形,就构成了包围盒的侧面。所有正面朝向光源的三角形,就构成了包围盒的近顶。沿着包围盒侧面的方向,把各个近顶面分别推向无限远处,并且翻转朝向,就构成了包围盒的远底。只需在生成轮廓线的代码基础上稍作修改,即可得到动态生成包围盒的Geometry Shader,代码如下:
#version layout (triangles_adjacency) in; // six vertices in
layout (triangle_strip, max_vertices = ) out; // 4 per quad * 3 triangle vertices + 6 for near/far caps in vec3 PosL[]; // an array of 6 vertices (triangle with adjacency) uniform vec3 gLightPos; // point light's position.
uniform mat4 gProjectionView;
uniform mat4 gWorld; float EPSILON = 0.0001; // Emit a quad using a triangle strip
void EmitQuad(vec3 StartVertex, vec3 EndVertex)
{
vec3 LightDir;
LightDir = normalize(StartVertex - gLightPos);
// Vertex #1: the starting vertex (just a tiny bit below the original edge)
gl_Position = gProjectionView * vec4((StartVertex + LightDir * EPSILON), 1.0);
EmitVertex();
// Vertex #2: the starting vertex projected to infinity
gl_Position = gProjectionView * vec4(LightDir, 0.0);
EmitVertex(); LightDir = normalize(EndVertex - gLightPos);
// Vertex #3: the ending vertex (just a tiny bit below the original edge)
gl_Position = gProjectionView * vec4((EndVertex + LightDir * EPSILON), 1.0);
EmitVertex();
// Vertex #4: the ending vertex projected to infinity
gl_Position = gProjectionView * vec4(LightDir , 0.0);
EmitVertex(); EndPrimitive();
} void main()
{
vec3 worldPos[]; // vertexes’ position in world space.
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
worldPos[] = vec3(gWorld * vec4(PosL[], 1.0));
vec3 e1 = worldPos[] - worldPos[];
vec3 e2 = worldPos[] - worldPos[];
vec3 e3 = worldPos[] - worldPos[];
vec3 e4 = worldPos[] - worldPos[];
vec3 e5 = worldPos[] - worldPos[];
vec3 e6 = worldPos[] - worldPos[]; vec3 Normal = normalize(cross(e1,e2));
vec3 LightDir;
LightDir = normalize(gLightPos - worldPos[]); // Handle only light facing triangles
if (dot(Normal, LightDir) > ) {
Normal = cross(e3,e1);
if (dot(Normal, LightDir) <= ) {
EmitQuad(worldPos[], worldPos[]);
} Normal = cross(e4,e5);
LightDir = normalize(gLightPos - worldPos[]);
if (dot(Normal, LightDir) <= ) {
EmitQuad(worldPos[], worldPos[]);
} Normal = cross(e2,e6);
LightDir = normalize(gLightPos - worldPos[]);
if (dot(Normal, LightDir) <= ) {
EmitQuad(worldPos[], worldPos[]);
} // render the front(near) cap
LightDir = (normalize(worldPos[] - gLightPos));
gl_Position = gProjectionView * vec4((worldPos[] + LightDir * EPSILON), 1.0);
EmitVertex();
LightDir = (normalize(worldPos[] - gLightPos));
gl_Position = gProjectionView * vec4((worldPos[] + LightDir * EPSILON), 1.0);
EmitVertex();
LightDir = (normalize(worldPos[] - gLightPos));
gl_Position = gProjectionView * vec4((worldPos[] + LightDir * EPSILON), 1.0);
EmitVertex();
EndPrimitive(); // render the back(far) cap
LightDir = worldPos[] - gLightPos;
gl_Position = gProjectionView * vec4(LightDir, 0.0);
EmitVertex();
LightDir = worldPos[] - gLightPos;
gl_Position = gProjectionView * vec4(LightDir, 0.0);
EmitVertex();
LightDir = worldPos[] - gLightPos;
gl_Position = gProjectionView * vec4(LightDir, 0.0);
EmitVertex();
EndPrimitive();
}
}
EmitQuad
上文提到,包围盒的远底面是位于无限远处的。这是数学意义上的描述。具体到OpenGL,其实并不需要描述一个无限远的顶点,只需要找到此顶点投影到近裁剪面上的投影位置即可。简单来说,只需将从光源到轮廓线上的顶点的向量作为xyz坐标,以0为w坐标,即可得到此投影位置。
从数学上理解此问题需要一些晦涩的推导过程,这里从OpenGL Pipeline的角度来理解即可。一般的,OpenGL描述顶点位置都是用vec4(x, y, z, 1)。在Pipeline从Clip Space到NDC Space的变换过程中,会将所有顶点的xyz坐标都除以w,所以vec4(x, y, z, w)、vec4(x/w, y/w, z/w, 1)和vec4(nx, ny, nz, nw)描述的都是同一个位置。试想,如果保持xyz的值不变,而不断减小w的值,那么这个坐标描述的位置会越来越远;当w减小到0时,这个坐标描述的就是一个无限远的位置。那么,沿着光源L到顶点的方向,走到无限远的那个位置,只能是vec4(LightDir, 0)。
判断
使用Stencil Buffer和Depth Buffer实现阴影的渲染的过程如下伪代码所示:
void ShadowVolume(Scene scene, ..)
{
// Render depth info into depth buffer.
RenderDepthInfo(scene, ..); glEnable(GL_STENCIL_TEST); // enable stencil test.
glClear(GL_STENCIL_BUFFER_BIT); // Clear stencil buffer.
// Extrude shadow volume and save shadow info into stencil buffer.
{
glDepthMask(false); // Disable writing to depth buffer.
glColorMask(false, false, false, false); // Disable writing to color buffer.
glDisable(GL_CULL_FACE); // Disable culling face. // Set up stencil function and operations.
glStencilFunc(GL_ALWAYS, , 0xFF);
glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP);
glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP); // Extrude shadow volume.
// Shadow info will be saved into stencil buffer automatically
// according to `glStencilOp...`.
Extrude(scene, ..); // Reset OpenGL switches.
glEnable(GL_CULL_FACE);
glColorMask(true, true, true, true);
glDepthMask(true);
}
// Render the scene under the light with shadow.
{
// Set up stencil function and operations.
glStencilFunc(GL_EQUAL, 0x0, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); // light the scene up.
RenderUnderLight(scene, ..);
}
glDisable (GL_STENCIL_TEST); // disable stencil test.
}
Shadow Volume由3遍渲染完成。
第一遍渲染时,在不考虑阴影的前提下正常渲染场景。此时,Depth Buffer填充了正常的深度信息。这一次渲染的目的是准备好这一深度缓存,渲染的颜色并不重要。因此可以用最简单的Fragment Shader,甚至不使用Fragment Shader。
第二遍渲染前,启用模板测试,并按如下方式设置模板测试的函数和操作:
// Always pass stencil test.
glStencilFunc(GL_ALWAYS, , 0xFF);
// If depth test fails for back face, increase value in stencil buffer.
glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP);
// If depth test fails for front face, decrease value in stencil buffer.
glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP);
这里设置模版测试对于每个像素都是通过的,且通过后将对应像素位置的模板缓存值设置为0。当模板测试完成后,对于包围盒背面的Fragment,如果深度测试失败,那么模版缓存的值加1;对于包围盒正面的Fragment,如果深度测试失败,那么模板缓存的值减1。
这样设置的结果是,位于包围盒内部的模型(或其一部分),包围盒的背面的深度测试会失败,所以此处的模板缓存值加1;包围盒正面的深度测试会成功,所以对模板缓存无影响。比包围盒更靠近Camera的模型(或其一部分),包围盒的正面背面的深度测试都会失败,所以此处的模板缓存值加1又减1,保持为0。比包围盒更远离Camera的模型(或其一部分),包围盒的正面背面的深度测试都会成功,所以此处的模板缓存值保持不变,即为0。而在包围盒涉及不到的位置,模板缓存也保持不变,即为0。
也就是说,只有包围盒内部的模型(或其一部分)对应的模板缓存值是大于0的,其它位置的模板缓存值都保持为0。而包围盒内部的模型(或其一部分)就位于阴影中。所以第二遍渲染的只有包围盒,这样就能区分出阴影部分,如下图所示:
Shadow Volume判断阴影
如图所示,场景中有一个点光源L位于左上角,一个地板(Floor)上方漂浮着一个立方体(Cube)。光源L照射到Cube和Floor上,Cube投射出自己的阴影,这阴影由包围盒描述处出来。图中ABCD都代表Floor上的一点。A点位于包围盒内部,包围盒的背面的深度测试会失败,所以此处的模板缓存值加1;包围盒正面的深度测试会成功,所以对模板缓存无影响。B点比包围盒更靠近Camera,因此此位置上的包围盒的正面背面的深度测试都会失败,所以此处的模板缓存值加1又减1,保持为0。C点比包围盒更远离Camera,包围盒的正面背面的深度测试都会成功,所以此处的模板缓存值保持不变,即为0。D点与包围盒的任何一部分都没有交集,因此不受包围盒影响,模板缓存在此位置的值保持不变,即为0。
包围盒本身是一个模型,但并不存在于原本的场景中,所以在第二次渲染过程中要通过下述代码来避免将其渲染到最终的场景中:
glDepthMask(false); // Disable writing to depth buffer.
glColorMask(false, false, false, false); // Disable writing to color buffer.
这样就保证了包围盒不改变深度缓存,也不会写入颜色缓存。同时,其他功能仍然能够正常进行。
为了保证包围盒的正面背面都被渲染,需要禁用背面剔除功能:
glDisable(GL_CULL_FACE); // Disable culling face.
第三遍渲染前,重新设置模板测试的函数和操作:
// Draw only if the corresponding stencil value is zero.
glStencilFunc(GL_EQUAL, 0x0, 0xFF);
// prevent updating to the stencil buffer.
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
此时设置模板测试仅允许模板缓存值为0的位置通过。也就是说,只有在第二次渲染时位于包围盒外部的模型(或其一部分)才会被渲染并可能影响到最后的Framebuffer。此时已经无需(也不应)修改模板缓存的值,所以设置在任何情形下都让模板缓存的值保持不变。
这时,只需按通常的方式用光照模型渲染场景,即可产生即有光照又有阴影的最终效果。
多光源下的阴影
无论Shadow Mapping还是Shadow Volume都可以简单地应用到有多个光源的场景中。类似于多光源下的光照模型Blinn-Phong,只需分别对每个光源执行一遍Shadow Mapping或Shadow Volume,并且用混合功能将各个光源的照射效果叠加即可。其伪代码如下:
void MultipleLights(Scene scene, ..)
{
// render ambient color. foreach (var light in scene.Lights)
{
// preparation. glEnable(GL_BLEND); // enable blending.
glBlend(GL_ONE, GL_ONE); // add lighting info to previous lights. // light the scene up with specified light.
RenderUnderLight(scene, light, ..); glDisable(GL_BLEND);
}
}
下图展示了同时用红绿蓝三色光源照射模型的场景:
多光源照射的光和影(左)点光源(中)平行光(右)聚光灯
图中用三个小球描述了光源的位置。对于平行光,小球描述的是光源的方向。
总结
本文介绍了Shadow Volume渲染阴影的方法。Shadow Volume的实现相对复杂,对模型的规范性有一定的要求,但是阴影的分辨率很高。如果将Shadow Mapping类比作位图,那么Shadow Volume可以类比作矢量图。
r Shadow Mapping的思路是什么?
两遍渲染:首先从光源位置渲染场景,得到深度缓存;然后依据深度缓存判断Fragment是否位于阴影中。
r Shadow Volume的思路是什么?
两遍渲染:首先动态生成阴影包围盒,并设置模版缓存;然后依据模版缓存的状态判断Fragment是否位于阴影中。
r 多光源的阴影如何实现?
依次对每个光源运用Shadow Mapping或Shadow Volume算法。
r Shadow Volume最可能的失败原因是什么?
创建OpenGL Render Context时没有指定创建模版缓存。
问题
带着问题实践是学习OpenGL的最快方式。这里给读者提出几个问题,作为抛砖引玉之用。
- 请在Github代码中的Demos\LogicOperation项目中尝试使用各种类型逻辑操作,观察各自的效果。注意,需要将鼠标移动到Cube模型上才能看到逻辑操作的效果。
- 任意选择一个示例项目,或者新建一个项目,尝试使用剪切测试(Scissor Test),观察效果。思考剪切测试能够帮助实现什么功能?
- 关于模版测试的示例项目Demos\StencilTest中,Cube的包围框的宽度随Cube的原理而逐渐减小。请尝试使用Shader来保证包围框的宽度保持不变。
CSharpGL(48)用ShadowVolume画模型的影子的更多相关文章
- 转载 用ShadowVolume画模型的影子
阅读目录(Content) Shadow Volume 包围盒 动态生成包围盒 判断 多光源下的阴影 总结 问题 CSharpGL(48)用ShadowVolume画模型的影子 回到顶部(go to ...
- CSharpGL(44)用ShadowMapping方式画物体的影子
CSharpGL(44)用ShadowMapping方式画物体的影子 在(前文)已经实现了渲染到纹理(Render To Texture)的功能,在此基础上,本文记录画物体的影子的方式之一——shad ...
- 48.Python中ORM模型实现mysql数据库基本的增删改查操作
首先需要配置settings.py文件中的DATABASES与数据库的连接信息, DATABASES = { 'default': { 'ENGINE': 'django.db.backends.my ...
- 有关基于模型的设计(MBD)一些概念和理解(zz)
http://www.matlabsky.com/thread-38774-1-1.html 本文转载于MathWorks中国高级工程师董淑成的帖子内容.为了方便阅读,对原文进行了重新整理编辑. 之前 ...
- [UE4]解决角色影子的问题
一.自己应该是不能看到第一人称模型的影子,只要将第一人称模型影子不投影的就可以了.Cast Shadow勾选去掉就不会有影子了. 二.在第一人称视角中,枪支也是不应该有投影的.在weanpon中新增2 ...
- XNA 中3D模型的显示
XNA 中3D模型的显示: ModelMeshPart[] meshParts; Model start_model; Matrix[] dq_model_transforms; Matrix vie ...
- opengl画不出直线 线段 坐标轴 却能画出其他图形的坑
原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/12054507.html 好多次都是画坐标轴的三条直线画不出来,虽然最后都解决了 但是还是耽误 ...
- 48、django工程(model)
48.1.数据库配置: 1.django默认支持sqlite,mysql, oracle,postgresql数据库: (1)sqlite: django默认使用sqlite的数据库,默认自带sqli ...
- CFD计算
47 求解器为flunet5/6在设置边界条件时,specify boundary types下的types中有三项关于interior,interface,internal设置,在什么情况下设置相应 ...
随机推荐
- Help Jimmy ~poj-1661 基础DP
Help Jimmy" 是在下图所示的场景上完成的游戏. 场景中包括多个长度和高度各不相同的平台.地面是最低的平台,高度为零,长度无限. Jimmy老鼠在时刻0从高于所有平台的某处开始下落, ...
- hdu1010 Tempter of the Bone---DFS+奇偶剪枝
题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=1010 题目描述:根据地图,'S'为开始位置,'D'为门的位置,' . '为空地,'X'为墙,不能经过 ...
- php过滤表单提交的html等危险代码
表单提交如果安全做得不好就很容易因为这个表单提交导致网站被攻击了,下面我来分享两个常用的php过滤表单提交的危险代码的实例,各位有需要的朋友可参考. PHP过滤提交表单的html代码里可能有被利用引入 ...
- 深入浅出理解 TCP/IP 协议 (一)
文章转自:https://www.cnblogs.com/onepixel/p/7092302.html TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入 ...
- C#扩展方法(转)
扩展方法使您能够向现有类型"添加"方法,而无需创建新的派生类型.重新编译或以其他方式修改原始类型." 这是msdn上说的,也就是你可以对String,Int,DataRo ...
- MyBatis(2)——MyBatis 深入学习
编写日志输出环境配置文件 在开发过程中,最重要的就是在控制台查看程序输出的日志信息,在这里我们选择使用 log4j 工具来输出: 准备工作: 将[MyBatis]文件夹下[lib]中的 log4j 开 ...
- javaIO流--Writer,Reader
Writer /** *<li> Writer中定义的一个重要的方法: * public void writer(String str)throws IOException; */ pac ...
- [C#] .NET Core/Standard 2.0 编译时报“CS0579: Duplicate 'AssemblyFileVersionAttribute' attribute”错误的解决办法
作者: zyl910 一.缘由 当创建 .NET Core/Standard 2.0项目时,VS不会像.NET Framework项目一样自动生成AssemblyInfo.cs文件. 而且,若是手工在 ...
- java面试3-对于java中值传递的理解(Hollis)
这是根据Hollis的直面java内容习得(有兴趣的可以加他微信公众号) 对于初学者来说,要理解java中的值传递很难理解,为什么说java只有值传递?那引用传递呢? java中的错误理解: 错误理解 ...
- ABP领域层知识回顾之---仓储
1. 前言 在上一篇博文中 http://www.cnblogs.com/xiyin/p/6810350.html 我们讲到了ABP领域层的实体,这篇博文继续讲ABP的领域层,这篇博文的主题是ABP ...