Rise的自留地

记录生活中的点滴,分享编程技术和游戏开发经验。

0%

引言:

GameBryo拥有一套复杂的材质系统,这套材质系统可以根据渲染对象的状态和属性生成不同的shader代码,提高了渲染流程的适应性,可以使你定义一套材质能适应多种渲染对象。同时,GameByro将shader的初始化和使用插件化,方便与美术工具集成,并且实现了平台无关性。为了实现这些目的,GameByro使用了一套复杂的机制,本文主要解析GameByro如何生成、编译并使用shader代码。

Shader

GameBryo的shader的接口封装在NiShader中,顶点数据流声明,常量表的访问,渲染状态的设置都是通过这个类(有点类似于D3Deffect)。在程序运行NiShader是由NiShaderFactory负责管理的,NiShaderFactory通过NiShaderLibrary从文件中创建shader,用全局性的map管理起来。NiShaderLibrary通过解析shader文本创建NiShader对象,并调用3D图形接口编译shader代码,将这个类以dll的形式封装,就可以作为插件来使用。NiShader类的创建可以通过解析文件来进行,也可以通过C++的类来定制,只需从NiShader上继承即可。GameByro为PC平台提供了一个NiD3DXEffectShaderLib库,这个库提供了解析shader文件和初始化shader对象的功能。用户只需按GameByro定义的格式编写shader代码的语意和注释,NiD3DXEffectShaderLibrary就会根据文本来创建NiD3Dshader对象,在应用程序中就可以通过Techinqe的名称来访问这个对象。通过这种机制,我们将shader文本文件放在相关美术工具指定的目录下,在工具中就可以使用这些shader,并且能够通过shader的语意和注释为相关参数和变量生成UI,方便美术调试。

WIN平台上的整个流程如下:

1. 应用程序在启动时会先初始化整个shader系统,接下来导入Shader解析库和加载库(dll的形式)。

2. 接下来应用程序将NiD3DShader的初始化工作委托给NiShaderLibrary来处理,NiShaderLibrary首先通过NiD3DXEffectLoader载入所有的shader文本文件,并通过NiD3DXEffectParser解析文本生成NiD3DXEffectFile对象,同时NiD3DXEffectLoader还负责将shader代码编译成二进制形式的GPU程序。

3. 最后由NiD3DXEffectTechnique负责通过NiD3DXEffectFile上的信息生成NiD3Dshader对象。

4. 所有的shader对象创建后,NiShaderLibrary的初始化就结束了,最后由NiShaderFactory负责统一管理。

材质:

NiMaterial为渲染对象生成和定义Shader,NiMaterialInstance为渲染对象分配 和Cach Shader。NiFragmentMaterial提供了一个Shader Tree框架,在它的继承类中可以使用这个框架搭建shader tree。这个机制允许NiFragmentMaterial根据对象不同的渲染状态生成不同的shader代码,Cach在内存中,并保存到磁盘文件。GameByro描述符的概念大量使用,包括前面提到的Shader解析过程也是通过描述符来传递信息。在材质系统中主要使用了NiMaterialDescriptor和NiGPUProgramDescriptor这个两个类做描述符,这两个类中保存的信息是兼容的,都是为了描述某种材质在渲染对象的某一特定渲染状态下所对应的GPU程序的特征。NiFragmentMaterial通过渲染目标的状态和属性生成NiMaterialDescriptor,并通过NiMaterialDescriptor查找匹配的shader,如果找不到,则通过shader tree生成相应的shader程序,并保存到磁盘文件中。当下一次应用程序启动时就可以通过这个文件直接创建NiShader对象。可以说通过NiFragmentMaterial生成的shader代码是为特定的渲染对象在特定的情况下量身打造的。

整个过程的详细流程如下:

1. 在每次渲染一个物体之前,NiMaterialInstance会先判断这个物体的shader程序是否需要更新,如果不需要更新,就直接返回当前Cach的NiShader;如果需要更新, NiMaterialInstance首先会根据物体的渲染状态为其生成一个NiMaterialDescriptor,然后将这个NiMaterialDescriptor和当前Cach住的NiShader进行比较,如果匹配仍然返回当前Cach的NiShader,如果不匹配,将获得shader的工作转交给NiMaterial进行。

2. NiMaterial首先通过这个NiShaderFactory 查询匹配这个NiMaterialDescriptor的NiShader,如果找不到,就通过NiMaterialDescriptor生成NiShader,同时生成一段Shader代码,并保存到以shader描述符中的特征码来命名对应的shader文件。

3. 当获得相应的NiShader对象后,NiMaterialInstance会调用NiShader的SetupGeometry接口,在这个接口中会进行顶点声明。

以下是NiMaterialInstance为Geometry选择shader的代码:

NiShader* NiMaterialInstance::GetCurrentShader(NiRenderObject* pkGeometry,

const NiPropertyState* pkState,

const NiDynamicEffectState* pkEffects)

{

if (m_spMaterial)

{

bool bGetNewShader = m_eNeedsUpdate == DIRTY;

if (m_eNeedsUpdate == UNKNOWN)

bGetNewShader = pkGeometry->GetMaterialNeedsUpdateDefault();

// Check if shader is still current

if (bGetNewShader && m_spCachedShader)

{

bGetNewShader = !m_spMaterial->IsShaderCurrent(m_spCachedShader,

pkGeometry, pkState, pkEffects, m_uiMaterialExtraData);

}

// Get a new shader

if (bGetNewShader)

{

NiShader* pkNewShader = m_spMaterial->GetCurrentShader(

pkGeometry, pkState, pkEffects, m_uiMaterialExtraData);

if (pkNewShader)

{

NIASSERT(m_spCachedShader != pkNewShader);

ClearCachedShader();

m_spCachedShader = pkNewShader;

if (!pkNewShader->SetupGeometry(pkGeometry, this))

ClearCachedShader();

}

else

{

ClearCachedShader();

}

}

m_eNeedsUpdate = UNKNOWN;

}

return m_spCachedShader;

}

如果想通过NiFragmentMaterial实现自己的shader tree就需要在NiFragmentMaterial提供的接口中实现自己拼装代码的逻辑,代码块由NiMaterialLibraryNode封装,NiMaterialLibraryNode既可以直接写C++代码来定义,也可以先写成XML脚本,再由专门的解析工具转换成C++代码。

由NiStandardMaterial生成的shader代码文件如下图所示:

clip_image002

文件名就是NiMaterialDescriptor的掩码,用来标识的shader代码的行为。

Shader代码的行为描述如下:

Shader description:

APPLYMODE = 1

WORLDPOSITION = 0

WORLDNORMAL = 0

WORLDNBT = 0

WORLDVIEW = 0

NORMALMAPTYPE = 0

PARALLAXMAPCOUNT = 0

BASEMAPCOUNT = 1

NORMALMAPCOUNT = 0

DARKMAPCOUNT = 0

DETAILMAPCOUNT = 0

BUMPMAPCOUNT = 0

GLOSSMAPCOUNT = 0

GLOWMAPCOUNT = 0

CUSTOMMAP00COUNT = 0

CUSTOMMAP01COUNT = 0

CUSTOMMAP02COUNT = 0

CUSTOMMAP03COUNT = 0

CUSTOMMAP04COUNT = 0

DECALMAPCOUNT = 0

FOGENABLED = 0

ENVMAPTYPE = 0

PROJLIGHTMAPCOUNT = 0

PROJLIGHTMAPTYPES = 0

PROJLIGHTMAPCLIPPED = 0

PROJSHADOWMAPCOUNT = 0

PROJSHADOWMAPTYPES = 0

PROJSHADOWMAPCLIPPED = 0

PERVERTEXLIGHTING = 1

UVSETFORMAP00 = 0

UVSETFORMAP01 = 0

UVSETFORMAP02 = 0

UVSETFORMAP03 = 0

UVSETFORMAP04 = 0

UVSETFORMAP05 = 0

UVSETFORMAP06 = 0

UVSETFORMAP07 = 0

UVSETFORMAP08 = 0

UVSETFORMAP09 = 0

UVSETFORMAP10 = 0

UVSETFORMAP11 = 0

POINTLIGHTCOUNT = 0

SPOTLIGHTCOUNT = 0

DIRLIGHTCOUNT = 0

SHADOWMAPFORLIGHT = 0

SPECULAR = 1

AMBDIFFEMISSIVE = 0

LIGHTINGMODE = 1

APPLYAMBIENT = 0

BASEMAPALPHAONLY = 0

APPLYEMISSIVE = 0

SHADOWTECHNIQUE = 0

ALPHATEST = 0

NiStanderMaterial就是根据这些掩码的数据来生成shader代码,用户可以通过重载GenerateVertexShadeTree、GeneratePixelShadeTree、CreateShader这些接口来定义自己的shader生成规则。

增加自己的渲染效果:

通过前几节我们可以了解到,想定义自己的材质,一是通过编写shader代码完成。在应用程序初始化的时候,这些shader代码会被初始化成NiShader对象,进一步的通过NiShader对象来初始化NiSingleShaderMaterial对象,并分配给渲染对象。在GameByro默认的渲染流程中,这些步骤都是自动进行的,美术只需在3DMAX插件中为几何体的材质指定Shader程序,导出到nif文件,应用程序就能正确加载并渲染;二是定义自己的NiMaterialFragment类,在类中定义如何生成shader,在应用程序运行时只要将这个类的实例指派给几何体,这个类就会自动为几何体生成shader。这两种方式对于美术人员来说,主要区别在于,采用第一种方法定义的材质,其渲染数据的设置必须严格符合shader代码中所需的数据,否则就会报错。(比如说,顶点数据流必须严格符合shader程序的定义,必须为shader中每个采样器提供格式正确的纹理);而采用第二种方法定义的材质,就有很高的容错和适应性,但是这种容错性和适应性需要自己写代码来完成,GameByro提供的NiStanderMaterial就提供了这套完整的机制。每个贴图槽内的贴图如果你设置就会生成相应的贴图处理流程,如果不设置,就没有这张贴图的处理流程。

为了验证这个过程,笔者尝试增加了一个自己的shader特效——SubSurfaceScattering,简称3s,其原理是模拟光在半透明物体中散射的效果。由于该效果无须预处理过程,所有的贴图均来自磁盘文件,所以比较容易融合到GameByro工作流中。

笔者将在FX COMPOSER中调试通过的fx文件放入SDK中的SDK\Win32\Shaders\Data目录下,在3DMAX的材质面板选择GameByroShader,然后就可在显示shader的组合框中看到文件中定义的Techinqe,选择点击apply按钮,就会出现自定义的参数调整界面。

clip_image004

clip_image006

通过调整参数,最终得到皮肤和玉器的渲染效果如下:

clip_image007

皮肤

clip_image009

玉器

总结

GameByro的这套开发流程非常方便直观,但是美术仅能为shader程序分配静态的数据源,比如说光照图等,CubeMap等;而一些在程序中实时生成的纹理数据则无法整合到美术工具中,比如说阴影图、折射图、反射图等,这些都需要程序写代码来实现。调试起来就不大方便了。大部分情况下,我们只需要使用GameByro提供的NiStanderMaterial就可以完成大部分材质的需求,特殊的效果可以自己写shader或者通过引擎提供的shader库来完成,只有当我们需要即根据复杂的情况做很多不同的处理时,我们才需要重载NiFragmentMaterial搭建自己的shader tree。不过搭建shader tree的程序一般比较复杂,编写难度大,虽然引擎允许通过XML文件来编写材质节点,但是使用起来仍然不方便。GameByro并没有提供相关的后期处理的开发工具,后期处理的特效并不能所见即所得,这方面还需完善。

GameByro为几何体在特定的环境下生成专用的shader代码,具有一定的灵活性,但是也付出了以下代价:

l 分析几何体的属性和当前状态,为其生成shader代码的过程有性能损耗。

l Shader代码生成后会保存到磁盘文件中,这个过程如果不使用异步,可能会引起阻塞。

l 生成的NiShader对象会有内存消耗。由于GameByro默认的实现是将所有的shader文件初始化成NiShader对象,所以当游戏运行的时间久了以后会生成大量的shader文件,这时候内存的消耗可能会很可观,同时加载的时间也会增加。不过可以自己控制加载的流程,在这里进行性能优化。

引言:

GameByro作为一款次世代引擎,使用了复杂的材质系统,用来满足各种各样的需求。材质代表了物体受到光照后所呈现出的质感,而这种质感在计算机图形学中需要着色代码来完成,所以当前流行的图形引擎设计是使用被渲染对象的材质与shader相关联,GameByro也不例外。GameByro的材质系统可以通过shade tree生成shader程序,增强了应用程序层对可编程渲染管线的控制能力。

渲染架构概览:

在GameByro中,对象表面的色彩、纹理、光滑度、透明度、反射率、折射率、发光度等可视属性与传统的材质系统分离,独立的成为了对象的渲染属性(NiProperty),而材质(NiMaterial)仅用来对着色程序的封装,这样就实现了渲染数据和渲染方法的分离,降低了耦合性。如上所说的这些可视属性在Gamebyro中会封装成一个属性对象,在应用程序中如果对对象挂载这个属性对象,在GPU程序中就可以访问这个属性对象的值。渲染属性对象可以在创建时指定其类型,如纹理、浮点、矩阵、向量或数组,此外一些全局性的对象也可以通过在Shader中用语意声明为全局object对象,如灯光和摄影机等,这样就可以以同样的方式来访问这些对象上的属性。

GameByro每一帧的渲染(NiRenderFrame)划分为多个步骤(NiRenderStep),每个步骤又包含很多个批(NiRenderClick),NiRenderFrame封装了上层对渲染系统调用的接口,而NiRenderClick则代表了图形硬件的一次绘制操作(对渲染队列中所有的对象的顶点缓存调用DrawPrimitive),当应用程序调用NiRenderFrame的Display接口时, NiRenderFrame会依次调用每一个NiRenderStep的Render()接口,NiRenderStep就会执行所有的NiRenderClick操作。

对于每个NiRenderClick来说,首先要设置视口和渲染目标,也就是渲染数据流的入口和出口。视口建立以后就可以通过关联的摄影机对场景图中的对象进行裁剪(默认的有视口裁剪和遮挡裁剪,此外还可以通过回调函数加入自己的裁剪方式),将未被裁剪的对象放入渲染队列。然后Gambyro会根据材质来对渲染队列中的对象进行排序,让材质相同的对象处于相邻位置,这样可以减少切换shader的开销。

clip_image002

如图所示为帧渲染系统的结构图(简化版)

材质系统:

GameByro中的材质代表渲染对象所采用的方法。前面说过。纹理属性包含了着色所需的原料,那么材质就指定了对这些原料的加工方法。基于当前可编程渲染管线设计,材质就成为连接对象与GPU程序的中间层,应用程序可以通过材质将shader应用于几何体。

NiMaterial类是所有材质的基类,这个类通过一个Map来保存当前应用程序中所有NiMaterial的指针,当然这个Map是静态也就是说相当于全局变量,通过static NiMaterial* NiMaterial:: GetMaterial(const NiFixedString& kName)接口对这个全局的Map进行访问。也就是说,当前环境中所有的NiMaterial对象是通过NiMaterial类来管理的。此外NiMaterial类还通过静态成员变量保存了一个工作路径(即shader文件路径),通过这个路径加载shader文件。NiMaterial就像是一个中介,全权代理对对象的渲染工作。用户可以通过重载NiMaterial来实现自己的渲染机制。每个NiMaterial都是全局性的,可以作用于多个甚至是所有的渲染对象,但一个渲染对象也可以拥有多个NiMaterial,但只能有一个处于激活状态的NiMaterial。

NiMaterial的派生类NiFragmentMaterial提供了对可编程渲染管线完整的控制机制,内部保存了NiShader的哈希表、一个NiGPUProgramCache数组。并且NiFragmentMaterial会生成一个用来编译GPU程序的shade tree(后面会有解释)。这样的话,每个NiFragmentMaterial可以对应多个shader程序,这样就提供了一种机制,在运行时根据不同的运行环境和渲染对象不同的状态,来选择合适的shader程序。在NiRenderClick依次渲染可见集中的每个对象时,首先会判断其是否需要被渲染的标记(flag),如果需要被渲染,则使用NiMaterial::IsShaderCurrent接口判断当前shader(上一次渲染所使用的shader)是否有效,所谓有效就是仍然存在并且可以应用于本次的渲染对象,如果无效,则会调用NiMaterial::GetCurrentShader获得shader,用于本次渲染。NiMaterial::GetCurrentShader会根据渲染对象的属性和当前环境硬件条件来选择合适的shader程序。当然,可以通过重载IsShaderCurrent和GetCurrentShader接口来指定自己的有效性判断规则和如何选择shader程序的方案。 NiFragmentMaterial提供了一套搭建shade tree的框架,用户可以通过重载来搭建自己的shade tree,当然,如果不想通过shade tree的形式生成shader程序也可以,使用NiSingleShaderMaterial可以从文件生成shader程序。

Shade Tree

什么是shade tree呢?我们通常编写的shader代码是线性执行的,即每个pass流程执行的是文本上定义好的shader流程,每一段shader功能模块是按一定顺序依次执行的。如果需要修改流程中的某一部分就需要更改相关的shader代码并重新编译。而shade tree将shader代码以树形结构组织起来,每一个shader代码块(一般是一个函数)都会被编译成一个节点,通过定义输入变量和输出变量来提供数据流的入口和出口,这些节点的插入和删除可以通过应用程序来控制,从而灵活的控制整个渲染过程。这样shader程序中的一些核心模块可以由美术通过工具生成,然后插入到shade tree中,只要输入和输出的接口不变,就无须修改其他代码,从而降低了美术开发shader的门槛。

GameByro通过以下几个类搭建shade tree:

l NiMaterialConfigurator:shade tree被封装在这个对象中,Uniform constants被封装在NiMaterialResource中,而NiMaterialNode封装了相关的shader代码,所有的资源和节点通过NiMaterialResourceBinding连接起来。当所有的连接都确立以后,NiMaterialConfigurator会调用Evaluate接口生成GPU程序和一个输入Uniform资源的集合。

l NiMaterialFragmentNodes:这个类包含了一个shader代码片段的集合,这些代码片段为不同的平台和编程语言所编写。这就为shader程序员提供了更大的灵活性,用以控制他们的代码在不同平台和图形硬件上的表现。例如:在高端平台可以采用高级的shader model提供更好的效果,而在低端平台上可以关闭一些特效来加快速度。

l NiMaterialNodeLibraries:这个类是一个NiMaterialNode的集合,其实也就是一个shader库。它允许shade tree节点完全基于数据驱动。shader库的生成可以通过两种方式,解析XML文件或用XML文件生成C++代码。GameByro提供了相关的解析器和代码生成器。

l NiMaterialResources:shade tree中的Uniform constants,支持多种数据类型,包括Constant、Predefined、Attribute、Global、Object。

固定管线的渲染:

GameByro支持固定管线的渲染,其纹理混合过程如下。

clip_image004

固定管线的着色处理流程

上图很清楚的显示出了每个stage的操作,平行的表示两张纹理的采样是同时进行的,特定情况下右边的纹理可能被忽略。

大部分情况下,应用程序不会使用上面所有的stage,开启或者关闭那个stage可以由应用程序来指定。

以下为多重采样的原理图:

clip_image006

固定管线的纹理多重采样

缺省的着色处理流程:

GameByro提供了一个默认的着色处理流程,封装在NiMaterial的派生类NiStandardMaterial中,这个类执行类似于固定管线的流程,在不同阶段将纹理采样、并将采样到的数据混合到最终的结果中去。

GameByro默认的材质系统的特性如下:

  • Skinned and unskinned transformations. Skinned transformations can support up to 30 bones per draw call.
  • Vertex colors
  • Base maps
  • Normal maps
  • Parallax maps
  • Dark maps
  • Detail maps
  • Bump environment maps
  • Gloss maps
  • Glow maps
  • Decal maps (up to 3)
  • Cubic and spherical environment maps
  • Point/Spot/Directional/Ambient lights contributing to the diffuse, specular, and ambient color. Up to 8 total lights. Per-pixel or per-vertex.
  • Projected light maps. Clipped or unclipped. (Up to 3)
  • Projected shadow maps. Clipped or unclipped. (Up to 3)
  • Texture transforms per map.
  • Per-vertex fog

下图显示为不同的纹理、灯光、材质属性的组合过程,不过需要注意的是,视差贴图和凹凸贴图属于特殊的情况,它们仅仅影响到纹理采样的UV坐标,而并非直接对最后的颜色值产生贡献。视差贴图会改变所有贴图采样的UV坐标,而凹凸贴图仅对环境贴图的UV产生影响。

clip_image008

以上流程完全由shade tree构建,NiStandardMaterial提供了大量的函数接口用于对每个流程的控制,用户可以通过重载相关的接口,插入自己的shade tree节点,修改每一步的操作或处理过程。例如:

virtual bool HandleBaseMap(Context& kContext, NiMaterialResource* pkUVSet,

NiMaterialResource*& pkDiffuseColorAccum,

NiMaterialResource*& pkOpacity, bool bOpacityOnly);

当然,整个流程的顺序和结构修改起来比较困难,如果有需要可以定制自己的材质系统,搭建自己的shade tree。

NiStandardMaterial提供了若干回调函数,这些函数可以动态的修改流程,分割PASS,对shader运行失败进行容错。

l SplitPerPixelLights/SplitPerVertexLights:这两个函数分别作用于逐顶点光照和逐像素光 照,当物体所受的光源数量太多,超过了顶点或像素着色器的能力时,通过这些函数可以将失败的pass分割成两个,如果分割出的pass仍然不能执行,那么函数会被递归调用,直到每个pass只有一个光源为止。

l SplitTextureMaps:这个函数会把对纹理采样的pass进行分割,当纹理查询过多时,顶点或像素着色器就会过于复杂,这时就可能导致shader运行失败。此函数只能迭代一次,生成一个额外的pass。

l DropParallaxMap:这个函数用来从几何体上移除视差贴图,且不产生额外的pass。

l DropParallaxMapThenSplitLights:这个函数首先调用DropParallaxMap移除视差贴图,然后一直调用SplitPerPixelLights直到失败为止。

NiMaterialInstance:

NiMaterial并不是直接与NiRenderObject相关联,而是经过了NiMaterialIstance这个中间层,由它来代理将NiMaterial关联到几何体,它负责调用NiMaterial为NiRenderObject生成NiShader,通过改变NiMaterialIstance上的接口SetMaterialNeedsUpdate,可以决定每一帧NiRenderObject所使用的材质是否需要被更换,通过接口SetDefaultMaterialNeedsUpdateFlag,可以决定当前材质所需的数据(即当前渲染流程所需的数据)是否要被更新。这样每个NiMaterialIstance只能被一个NiRenderObject所拥有,而多个NiMaterialIstance可以共享一个NiMaterial,这样就减少了重复创建NiMaterial的时间和空间上的开销,同时降低了渲染对象个材质之间的耦合度。

如下为材质系统的类结构简化图:

clip_image010

渲染属性:

前面提到过,GameByro将渲染所需要加工的数据全部封装在了NiProperty中,只要在shader中用指定的语法进行声明,就可以访问这些属性的值。

目前引擎中已经定义了12种属性,均派生自NiProperty,分别代表渲染数据的12种不同类型:

l NiAlphaProperty

l NiDitherProperty

l NiFogProperty

l NiMaterialProperty

l NiRendererSpecificProperty

l NiShadeProperty

l NiSpecularProperty

l NiStencilProperty

l NiTexturingProperty

l NiVertexColorProperty

l NiWireframeProperty

l NiZBufferProperty

用户也可以自定义属性类型,但所对应的数据类型要被shader语言所支持。

光照与阴影:

光照与阴影密不可分,因为阴影就是由光照产生的,前面在材质系统中已经提到过光照对着色的影响,这里重点阐述,GameByro是怎样根据光源产生阴影的。由GameByro提供的阴影均基于ShadowMap技术,

但也提供了ShaowVolume的示例代码。

Shadowing System是完全建立在帧渲染系统上的, 通过一个RenderClick生成ShadowMap,然后在正常渲染流程开始之前将ShadowMap更新到可见集内每一个渲染对象上。这样当渲染对象使用NiStandardMaterial时就会根据光源的阴影技术来对ShadowMap进行采样,并将结果与最终的输出颜色按一定比例混合。

阴影系统由以下几个类构成:

GameByro提供了三种类型的Shadow Write Materials,分别为NiPointShadowWriteMaterial、

NiDirectionalShadowWriteMaterial、NiSpotShadowWriteMaterial,适用于三种不同的光源类型。

  • Shadow Technique:这个类封装了阴影算法的细节,包括生成ShadowMap和使用ShadowMap投射阴影。
  • Shadow Render Click: 这个类是一个生成ShadowMap的批,这个类的对象是由shadow click generator负责生成。
  • Shadow Click Validator: Shadow Render Click:通过此类对象判断接受阴影的几何体对于shadow generator来说是否可见。
  • Shadow Map: 每个ShadowMap对象包含一个作为阴影图的纹理,shadowmap对象由shadowManager直接管理,每个shadowmap对象都被一个shadow generator引用。
  • Shadow Cube Map: 同shadowmap作用相同,只是阴影图的纹理类型为CubeMap。主要用于点光源生成的全方向阴影。
  • Shadow Generator: 阴影生成器,每个ShadowGenerator都对应一个NiDyamicEffect(NiLight的基类),也就是为这个NiDyamicEffect代表的光源生成阴影, 生成阴影所采用的技术由对象引用的ShadowTechnique来决定。
  • Shadow Click Generator: 生成ShadowMap的Shadow Render Click都由此类负责创建。这个类为每个ShadowGenerator指定ShadowMap,并负责每帧更新ShadowMap和ShadowMap所对应的变换矩阵。
  • Shadow Manager:所有的ShadowMap、ShadowTechnique、ShadowGenerator、shadow render click对象都由ShadowManager统一管理,并负责使用一个shadow click generator 在每一帧生成一个shadow render click的列表。

阴影系统静态结构如下:

clip_image012整个阴影渲染的流程大致如下:

1. 在应用程序初始化阶段,通过调用NiShadowManager的Initialize()接口实现对整个阴影系统的初始化,此时应用程序会注册所有的ShadowTechnique,并初始化NiShadowClickGenerator。

2. 当我们创建一个NiLight以后,我们可以通过NiShadowManager为这个NiLight新建一个NiShadowGenerator,NiShadowGenerator会通过NiLight的类型来选择合适的NiShadowTechnique,此时NiShadowManager会为新的NiShadowGenerator创建一个NiShadowRenderClick。

3. 当帧渲染系统启动后,NiShadowRenderClick的PerformRendering()接口会被调用,此时NiShadowRenderClick会通过引用的NiGenerator获得阴影生成的着色程序和所需的数据(例如深度偏移),同时通过NiGenerator引用的Camera获得场景图中的可见集。下一步就是对可见集中的渲染对象添加ShadowWriteMaterial并设为激活状态,而ShadowWriteMaterial的类型是根据NiDyamicEffect的类型指定的。最后NiRenderClick就会启动渲染流水线,将可见集中的对象的深度全部渲染到ShadowMap中。

4. 在第一个RenderClick中生成了ShadowMap,下面就要使用这些ShadowMap投射阴影。在每一帧开始之前,用户还可以自己指定不接受阴影的节点,手动将其插入NiShadowGenerator::m_kUnaffectedReceiverList中。在渲染BackBuffer的RenderClick中,首先会对场景图中的节点进行一次遍历,将不受阴影的节点放入NiShadowGenerator:: m_kUnaffectedCasterList的列表。在对每个节点进行渲染时,会遍历NiShadowManager中所有的NiShadowGenerator,判断这些NiShadowGenerator是否对这个节点有影响,判断的规则是此节点是否存在于UnaffectedReceiverList和UnaffectedCasterList这两个链表中,如果存在于任何一个链表,则节点不受此NiShadowGenerator影响,如果受此NiShadowGenerator影响,那么就将该NiShadowGenerator上的ShadowMap和数据更新到节点上的渲染属性中,NiStandardMaterial会根据这些数据选择对ShadowMap采样的方式,并将结果混合到最终的输出颜色中。

值得注意的是,点光源的shadowMap默认的是采用CubeMap实现,用户可以通过接口选择不使用CubeMap实现,当采用CubeMap实现时,光源无法产生软阴影。

渲染系统的扩展:

为了验证GameByro渲染系统的扩展性,笔者尝试着加入了一个后期处理特效Screen Space Ambient Occlusion(SSAO),即屏幕空间的遮蔽,由于仅仅为了熟悉GameByro的渲染流程,所以笔者并未对SSAO算法做深究,仅仅用了自己简化的算法。

在渲染过程中先单独使用一个RanderClick将场景中的深度渲染到一张纹理上,然后在渲染到后台缓冲区的RenderClick中对深度纹理进行采样,执行SSAO算法,将结果混合到最终的结果中。采样点的偏移坐标是通过对一组随机向量进行归一化再乘以0~1之间的随机数而生成的,即长度为0~1之间的随机向量。

PS代码如下:

float VerticalRange:GLOBAL; //控制XY方向采样范围变量,可以在应用程序层对其进行调整

float HorizontalRange:GLOBAL; //控制在Z方向采样范围的变量

float calAO(float2 texCoord,float dw, float dh ) //通过当前像素所标和偏移量计算AO

{

float2 coord = float2(texCoord.x + dw, texCoord.y + dh);

float4 CenterPos = tex2D(DepthSampler,texCoord);

float4 CurPos = tex2D(DepthSampler,coord);

float depthDiff = clamp(CenterPos.z - CurPos.z,0,VerticalRange);

float ao = depthDiff/length(CurPos.xyz - CenterPos.xyz);

return ao;

}

// Pixel shader

float4 PS_SSAO(VS_OUTPUT In) : COLOR

{

float2 texCoord = In.BaseTex;

float depth = tex2D(DepthSampler,texCoord).z;

float ao = 0.0;

float scale = HorizontalRange/depth; //因为采样范围会受深度影响,故除以此系数。

for(int i=0; i<32; ++i)

{

float2 offset = arrRandomPt[i].xy* scale;

ao += calAO(texCoord, offset.x, offset.y);

}

ao/=32;

float4 color = tex2D(BaseSampler,texCoord);

color.xyz *= (1.0 - ao);

return color;

}

最终实现效果如下:

clip_image014clip_image016

SSAO生成的明暗图 无SSAO材质

以下两图上图为无SSAO效果,下图为开启SSAO后的效果。

clip_image018

clip_image020

总结:

GameByro的帧渲染系统是比较灵活,想加入自己的渲染流程是比较容易的,此外由于RenderTarget和RenderView都可以由用户指定,所以想实现自己的shader效果不是很难。然而,NiStanderMaterial的shade tree比较复杂,总共高达6000行代码以上,过程非常复杂,这就是说,如果想实现自己的材质处理流程也要付出相当大的工作量,阴影系统虽然实现了较低的耦合度,但是实现过于复杂,不够简洁高效。

作者:叶起涟漪

★ 你好,我去杀几个人,很快回来。
★ 我去后山和黑山老妖研究吃唐僧的事,有事回来再说。
★ 便秘中。。。。。勿搅。。。
★ 你终于来啦,我找你N年了,去火星干什么了?我现在去冥王星,回头跟你说个事,别走开啊
★ 您所呼叫的用户尚未安装QQ......
★ 你呼叫的人现在不在,当你听到硬盘“咔”的一声,请对着鼠标留言,谢谢......
★ 主人不在。到哪儿去了?就......就是不告诉你!真要找的话,请按住电脑power键4秒钟后留言......
★ 该用户没有回应,可能用户忙,请稍候再试。或按Ctrl+Alt+Del返回.
★ 自杀中,稍后再说...
★ 有事找我请大叫!
★ 你现在连接到的是海狼的冰箱,放入食物后请断线,谢谢合作。
★ 我正在下五子棋,上次分心把裤子输光了。这次再输会被拍裸照!我可不想让人看见,等这盘下完了一定回你。
★ 嘀,这里是自动应答,MM请再发一次,我就与你联系;JJ请再发两次,我就与你联系;GG、DD就不要再发了,因为发了也不和你联系!
★ 腾讯服务器系统崩溃,请稍后再试!
★ 计算机正在处理你的信息,请稍侯,如果长时间没有响应,请重新启动计算机!
★ 你好,现在我在玩一个叫CS(或其他)的游戏,有事请按下电脑上“RESET”键,在听到“嘀”一声之后留言,谢谢
★ 你有权保持沉默,你所说的一切都将被作为存盘记录。你可以请代理服务器,如果请不起网络会为你分配一个。
★ 您使用的QQ是未注册版本。您可以继续使用本软件,但将屏蔽发送短信息的功能。支持国产软件,请您注册新版!
★ 广告之后,马上回来!
★ 正在澡澡哦~~不要偷看~~~嗯~啊~~嗯~啊~~~
★ 在加本人为好友前请详细阅读以下条款:1.未满18岁者请在父母指导下与本人聊天;2.非专业聊天人士,不承担主动招呼义务;3.谢绝你问我答制。在本人不回复的情况下请自觉停止一切信息!
★ 你所呼叫的用户正在系统整理,请稍后再呼。
★ 此人正在被国际刑警组织全球范围内通缉,如果您有此人的消息,请拨打当地报警电话120,请对着电话说如下密语言:我真的不是疯子,我不是疯子,我真的不是疯子!然后汇报你的所在地谢谢合作。我们的专业人员会去保护您的安全。
★ 您的QQ已经中了我安插的病毒,请不断向本人发信,否则本人一有闲暇立刻动手!
★ 您好,QQ今天休假,我是NETANTS......
★ 由于大气电离层影响,与该用户的卫星连接已中断,请稍后再试。
★ 对不起,由于服务器的原因,您刚才发的信息丢失,请重发一面。
★ 机主正在裸奔,机主已奔出服务区。
★ 正在格式化你的硬盘,请稍候......
★ 唉呀~你可真调皮!你看,我话还没说完你又来了~~
★ 嗨~我现在不在,如果有事请在听到“逼”的一声后留言······逼!
★ 对不起,网路不通,请重新发送...
★ 你要机主不在,请等到“嘟~”声后拿起鼠标留言。。。
★ 对不起,您所呼叫的用户不在计算机旁,请你现在立刻猛击你的显示器直到它冒火花为止,听到响声后我会给你回复
★ 你好。这是腾讯服务中心自动回复。收到这条信息表明您的操作系統出了问题。请按Ctrl+Shift+Del
★ 您所呼叫的用户先不再服务区内......可能是网络不通...想解决此类问题...请您将头用力撞向显示器......以保证网络通畅......
★ 本人现在位置:WC; 姿势:下蹲; 脸部:抽搐; 状态:用力中。。。。
★ 注意:使用腾讯移动QQ的用户可能无法立即获取你的留言。
★ 我去吃饭了,如果你是帅哥,请一会联系我,如果你是美女.................................就算你是美女,我也要先吃饱肚子啊
★ 你要和我说话?你真的要和我说话?你确定自己想说吗?你一定非说不可吗?那你说吧,这是自动回复,反正我看不见
★ 你好,我是XXX的自动回复,现在他不在,跟我说什么我也就这几句!
★抽筋中……请勿打扰……
★悟空啊,你想和为师说话么?想说话你就说呀,你不说为师怎么知道你想和为师说话呢?

超仔刚刚推门进来,屁股还没有碰到他的椅子上已经让人感觉到他欢喜轻飘的神色,我抬头望着他眼睛,神色中洋溢的满是欢快。我看着他那兴奋的样子,微微笑着问道:“签完了?结果还可以吗?”
“还不错!”
“能满意就可以,继续努力。”
“嗯。”
我知道超仔刚刚和公司签了新的合同,在新合同里他的工资有了一定的提高,这些都是因为对于他去年的绩效考核成绩还不错应该得到的结果。
年底对于我来说,可真是多事之秋,因为我需要在年底前完成对我团队这些人的一年的绩效评定,这些不但关系到他们年终的奖金,也影响到来年他们工资的涨浮。虽然自己一直讲究的是赏罚分明的做事方法,但是往往对于我来说要清晰分析哪个人在过去的一年中犯了什么样的错,对于这些错误去做一个评定,算其功过,工作不亚于一个项目的开发。一方面如果我评定的尺度太严格,那么最明显的一个影响就是年底年终奖金和来年的工资,在工作中涉及到这部分利益的东西处理起来本身就是敏感,所以自己一直都是平时严厉甚至苛刻,但是到这个时候,我反而会手下留情,只要态度对位,错误可以酌情考虑。对于我来说,一个人的态度决定他的一切。
“我的工作目标就是要替掉你。”这是超仔在面试的时候留给我印象最深的话,也因为这句话我发而对他有不少的好感。人最应该有的是有自信,超仔是刚刚毕业一年的新人,在面试时候做在我前面表情僵硬,全身紧张的他,在我问“你希望进入公司之后,能够有什么养的发展方向时?”这样的回答可能是一种自大,但是在我看来,这也是他的自信。也这是这种自信让我决定录用这个计算能力不算好,但是我觉得各种条件还算合格的小伙子。
当然我也非全凭一句“就要替掉你。”就把小伙子招聘进来,然后放到自己身边使劲搂拧,我想自己还不至于有这种变态的爱好。我喜欢面试的时候让应聘人员都手动填写一份简历,虽然很多时候他们自己都带了一份打印的现成简历,但我还是会让他们把公司那份表格填写完整,虽然很多时候他们自要把内容抄写过去就可以,但是这也是我对他们的第一个考验。很多程序员的字写的都有大师级的水平,对于他们来说,楷书,行书都不足以表达他们的意境,狂草,唯有这个才能体现他们的追求。有不少程序员在填写这个简历的时候,字迹就和天书一样,当然文字也像蛇一般的在纸上盘绕。
对于我来说,我只是通过这张简历看看他对自己的一个态度,我不要求他们的字能够达到书法家的水品,但是至少要工整,整个纸张让人看起来要干净清晰。我很难看到你对目前的工作有多少的渴望,但是我可以通过你写下来的文字看出来你是否重视这个事情,如果连工作机会都不重视,那以后的态度就值得担忧。所以通过这个我也放弃掉不少的人,虽然有些人在和我免谈的时候给我感觉也非常不错,但是如果两者结合在一起评定,如果后面没有做好,那么我只能认为他是学院派的,能说未必能做。
超仔长的不像程序员的样子,一米七五的个子,相貌也有几分帅气,留着平头短发,每日衣着新潮个性。他平时个性活泼开朗,又一次他还请了一天的假,说是去参加超男的选拔赛,结果回来之后,感慨道:“强人太多了,人外有人。”不过自己到没有为被淘汰的事情耿耿于怀,倒是很享受乐在其中的过程。
超仔进入公司的时间也只有半年左右,在这边年里面他不到两个月就给他转正了,比公司规定的三个月试用期提前了一个月多。肯学习和努力的态度让我对自己当初挑选他感到非常满意。在第一个月培训的时候,让他整理了以前遗留的旧的项目,那些项目本身缺少相关部分文档,但是由于项目一直在延续开发,所以久而久之,也就这么一直持续的做下来,那些已经对这些内容滚瓜烂熟的人也就没有心情去做,后来者也之能够在前辈的口传身教的情况下才能了解其中的奥妙,我一直希望能够把这部分资料补充完整。但是由于人手上不足,所以就一拖再拖。
公司对新人有比较详细的培训计划,在这些培训之外,我对超仔说:“你看看这个程序,因为你以后需要维护和开发这个,如果你有时间的话,可以把熟悉开发环境的步骤写成文档,文档的格式可以参考项目组内其他的模板,内容你线考虑一下,但是有一个要求,如果另外一个新人来接触这个程序,我需要他再看了你的稳当之后,就可以把环境搭配起来。些做的方式可以图文结合。”
结果在一周之后,超仔就给我说:“小余,我把那份文档写了一部分,你先看看有什么问题,如果不行的话我再修改。”
很多人都知道程序员非常讨厌写文档,所以很多程序员写出来的文档比写出来的代码还要难读,有时候读这种文档就是一种折磨,比读法律条款还难受,估计很多时候需要和法律文件一样,需要再配套上一本详细的解释才能把问题说明清楚。
但是超仔的文档让我非常满意的地方在两个地方,一方面再环境搭建的时候,用了截图的方式描述,而且每一幅图都是用作图工具简单的剪裁过,只保留关键部分,而避免乐像其他人那样是整个屏幕的图片,在么个图中对于操作关键区域还用红色正方形边宽标注说明,每个图片还有编号。
另外一个是整个文档是用Excel文档做成的,因为公司比较喜欢用Excel做给中开发文档,但我尝试着用打印预览的时候,让我惊奇的事情发生了,总共10页的文档,每一页都调整过,图片没有出现跨页的问题,而且打印的页头和页尾都修改过了,并没有保留上个文档的名称,作者和日期也都是正确的。
“页面打印是你调整过的?”看着文档,我轻声问道。
“我常常听到你让大家发邮件给客户的时候,要预览一下。所以我也就调了一下。”
听到这话,我心中是暗自窃喜,一个新人能够有这样的工作意识和态度,实属可造之才,技术可以学习培养,但是如果你想改变一个人做事态度其难度要远远大于技术上的培养过程。
超仔在后续半年的工作中也有犯了不少的错误,从刚刚开始技术上弱势,但是好学肯问,其进步的幅度不小,也使得他在很快的时间内能够达到对他所定位的能力要求。在于我看来这一切取决于他的态度,态度端正。
“态度端正,学习良好。”我的脑海中突然冒出这几个字,这不是小学时候,老师在每学期的期末成绩单上最喜欢用的两个评语。什么是态度?怎么才能算是端正呢?我在心中问了一下自己:“我的工作态度算不算端正呢?”或许像超仔这样的新人属于上升求学阶段,所以憧憬无限,但是对于我来说呢?当年何尝不是如此,现在还是不是一就有这样的心境呢?
“哎。”想着想着,不由自己叹了口气,难道我老了吗?怎么我还没有感觉到青春灿烂,这青春怎么就没了呢?看看坐在开发室中的其他人:老马,阿毛,木子,超仔,大师, 杰克这几个人,我们应该是活力无限的开发团队。
“Hi,小余,你在吗?”还没等我在继续发愣,MSN上的就弹出消息来。是上海的PM李。
我急忙回过去问有什么事情。
“我刚刚给你发了一封邮件,我把需要像客户移交的内容重新做了一个整理,你再看看有什么问题没有?”
我突然记起来,昨天晚上快接近9点的时候,我还在理发店修头发的时候,就接到李从上海给我打过来的电话,她问我现在有没有先前她发给我的计划列表,因为明天客户需要确认,所以她想今天晚上再把这份计划列表完善一下,如果我笔记本上有备份的话,给她转发一份,结果我刚好也只是把邮件接收到公司的机子上。最后我听到她在电话里说了一句:“那好,我明天早点去公司去修改一下,然后再给客户。”
李在一个月之前已经和公司提出离职的申请,因为她家庭的原因,她需要到国外发展。应该说再过两天她就会到美国去了,她本来是这个项目上海的项目经理,但是对于我来说,她是一个非常优秀的项目经理,从她身长我看到了一个项目能够成功的关键,态度。也就是她的这种工作态度,让整个项目有序稳定的进行。
在听完李的电话之后,我里一直佩服到,这才是工作态度。
作者:Yice(小余)
出处:http://www.yice800.cn

终于结束了近一个小时的枯燥会议,每周五公司级别的项目周会就像一个例行的检查,多少有点不痛不痒的味道,大部分的时候就是一个例行贯事,每个项目组按照顺序汇报一下各自项目组的情况和需要,如果不出现大的问题的话,也就是有本上奏,无本退朝的一个过程。或许有些人觉得这是一个比较浪费时间而且意义不大的会议,每周浪费大伙一个小时的时间去再次陈述这些本来在邮件中已经说明的问题和项目进度,不过存在就有价值,这个会议的最大的功能在于能尽量使各个项目组之间有部分的消息互通,至少能让大家了解到一点,其他人都在做什么,从而避免了每个项目组过于孤立的现象。
       今天的周会时间有点长,主要由于两件事情的讨论超出了会议的议程安排,第一件事情是大伟负责的项目目前进度出了问题,目前人员处在一个持续加班的紧张状态,即便如此,对于项目的最终Release时间还是比较确定的状态,用大伟的话来形容就是:“我们能够按时完成,但是可能会有点小问题。”一句比较含糊的托词,如果说套用官方的言词就是:“对于项目能够按时完成,我们还是谨慎乐观。”另外事情对于公司来说是一个好消息,公司将在下个月启动一个新的项目,这个项目是今年规模最大的一个项目,其业务方向主要针对美国的医疗领域,规模大概有150人/月。
这个项目将会被安排到我们项目组来处理,消息对于我来说就是一个挑战,对于自己来说,这个规模的项目也是超过了以往的一切项目,在会议上听到这个项目将由我来接手处理,不由小小的兴奋了一把,但是这股兴奋的感觉很快就被冲淡,自己粗略的考虑了一下项目组内目前的工作量和目前的人员情况,不由感觉到这个活不太好揽。不过有压力才有动力,这是我一贯的做事风格,类是这种明知会将苦重重的活,我越能够信心十足。我简单的在自己的记事本上写下了以下下周的关健工作内容:确认项目的具体开始时间和结束时间;调整目前项目组内的工作内容;向客户先要部分的开发相关资料;了解一下公司内部其它项目组的人员状况。
       会议后半程的内容我没有听多少,只是对于大伟的问题大家有部分的讨论,我才回过神来参与,大伟虽然说自己还是有信心能够保证项目的进度,但是为了保险起见,希望从其它的项目组借调两个人来协助他们做一些类是测试的工作,这样的话总的进度就可以进一步得到保证。以目前公司的架构模式,每个项目组相对独立的运作模式,如果说希望从其它的项目组借调人员,这种短期人员的借调有时候是比较困难,因为每个项目组目前来说都处于比较繁忙的阶段,目前闲暇的人员不多,即便其它项目组肯借人,大多放出去的人员也都是能力相对比较弱,正常情况下人不可能把项目组内部优秀的人员借调出去,这或许也是人的一种私心。
       在公司的协调安排之下,最后还是给大伟支援了两个人员,这才把讨论不休的会议打住。会议结束后我还在会议室坐了一会,我的脑袋里还在考虑下周的工作顺序,盯着自己的笔记本在对刚刚写的问题进行细化,把工作分解到下周的每一天。突然一股浓浓的烟味直接灌进我的鼻子,我不用抬头就能猜出是大伟,这个烟枪一天最少一包烟,如果遇到加班或则心情不好的话,估计两到三包都有可能。
       “少抽点烟,怎么心情不好?”看着大伟吐着烟圈,我问道。
       “没有,只是有点累。”大伟嘴里叼着烟,目无表情的应道。
       “我看到你们项目组的那些去年刚刚毕业的学生,最近是天天加班到晚上11点左右。这是怎么回事?”
       “噢,那时我故意的。”
       “故意?”我一时没有明白,疑惑的问道,“为什么要故意呢?”
       “我是按照我自己的工作能力给那些新人安排工作,如果说我是能够在上班时间内完成工作的话,那么那些新人肯定需要加班了。这样他们才能进步。”大伟的表情中开始露出得意的神色。
       “怪不得他们要加班,你工作了多少年,他们才工作多少年,再说了你拿的工资和他们那得工资能一样吗?”
       “我就告诉他们,如果你想工资那得和我一样,那么这些工作你能够准时完成,那么你就有机会。”大伟熄掉手中烟头,用双掌在脸上使劲得措动。
       “那你该不会是按照你的能力来做的项目估算吧?”对于大伟的这个逻辑我并不能苟同,但是我还是想了解项目的问题。
       “嗯…..”大伟支吾着回答到。
       “哈哈,你这是自作自受。”我对着大伟带着嘲讽的笑声说道。
       对于大伟的做法我却是无法苟同,虽然他的失误在于前期的估算过于乐观而且并不准确。但是对于给新人安排工作的方式上存在有比较大的问题,如果说前期的估算出现失误,这些错误在后期项目开始过程中还可以进行添加人员的方法进行补救的话,那么对于人员使用不当的问题,就像整个项目过程的绝症,其会影响到整个项目过程。
       项目组中开发人员从能力上有几个不同的级别,系统架构师(Systems Architect),高级工程师(Senior Engineer),中级工程师(Middle Engineer)和初级工程师(Junior Engineer)等等。对于每个级别上的人员的能力和开发技能都有一定的差异。这些就像我们手的手指一样,五指有长短,能力个不同。不同能力的人员应该在项目组中相应的工作内容和工作量。
       在正常的项目中我们讲究的是人尽其用,但是不能把一个项目在开始的时候就设想是通过发挥项目组人员的超水准的基础上来完成。很多人都说人在一定的压力之下,能够超能力的发挥出他们的水平,但是比代表说所有的人都能够在这种环境下能够有说突破,很多人在持续长时间的高压下会显得精神不振,萎靡的情况。所以作为项目管理来说,不能再项目一开始的时候就采用这个立足点来进行开发工作。
       对于大伟所说的那种歪论,我只能回应以自作自受的答案。我们给个人都是在逐步的学习和进步,如果说有一定压力灾,对于学习确实是一个比较有效的促进。但是作为项目经理来说,需要明白的是一个问题,我们的职责在于项目,而不是在于锻炼新人,新人进入项目组织后,通过项目过程的工作和学习能够提高他们的能力,这些是项目的副产物。而我们首先要保证项目的进度,质量。我在实际的项目开发过程中,对于给新人的工作计划都是按照其能力的80%-90%进行安排,很少会针对新人能力的100%进行工作任务分派,因为新人由于缺乏经验,所以在项目实际过程中如果遇到问题,他们解决这些问题的所采用的方法和思路都存在有时间的风险,有时候他们可能由于一个问题会难住他们,消耗掉很长的时间。即便我不断强调说如果有遇到问题,在一个小时内解决不了的话,就要把问题抛出来给大伙来共同处理,但是往往这个问题被最后迫不得已提出来的时候,都是在一天或则更长的时间之后。新人在开发过程中遇到这些问题的概率非常高,所以在工作安排工程中必须要考虑到这种风险。所着一旦推倒第一张加班的牌,那么后续的将是多米洛效应,加班将一发不可收拾,进入恶性循环阶段。
       对于实际的项目中我会采用减算安排工作的方式,那是因为我需要保留对项目整体的进度有更大的回旋缓冲时间。无论对新人还是对高级工程师,中级工程师和初级工程师我都会采用一样的策略,因为人脑不是机器,所以我们无法保证我们时时刻刻都能正常的满负荷运转。但是在平常的学习过程或者非实际项目的锻炼过程中,我会采用超负荷的工作安排模式,这个时候给人员的工作量安排都是100%-120%的程度,因为这个时候我的主要目的在于人员培养,所以需要一种超强度的环境,营造一种压力的气氛。其实开发过程何尝不是一个不见硝烟的战场,如果你想要在战场上不受伤,只有在平时多多受伤,这样在真实的战场上才能从容应对。
       对于项目组中我们需要比较清楚的了解每个成员的能力和个性,如果对于他们的能力了解能够像看着我们手指头那么清楚,我们就能够依据他们的长短,让他们协调发挥出水平来。我们需要避免工作不分能力,均等划分的安排方式,更不能把项目寄托在成员的超水平发挥的基础上。在工作中我们需要明白项目的最终目的,项目成功还是锻炼人员,从而采用减算还是加算得工作安排方式。
作者:Yice(小余)
出处:http://www.yice800.cn

你有没有考虑过自己能够管理多大的项目,能够带领多少人员的项目团队?5人?10人?100人?还是千军万马?但是在现实的项目中,能够带领100人员的项目经理未必能够带好10人的团队,反之亦然。因为作为软件项目来说存在有非常大的差异?无论你是大才还是小才,我们首先要清楚的认识到自己的才能是否能符合项目的实际应用,5人的项目和100人的项目团队中项目经理的工作重心必然不同,如果不区别对待,那么你的结局是大才小用,或者是小才大用。
项目的差异性
我没有机会参加类似IBM的OS/360规模的项目,我所能够参与的最大项目规模不过是100人/月上下的项目,当然也做过产品线的长期开发项目。所以对于那种巨无霸形的项目也只能是望梅止渴,对于其中的奥妙也只能捧读《人月神话》这类的经典,希望能够从中吸取精华来强壮自身。
项目的规模不一样,项目所能够配备给定的人员也不一样,对于大型的项目,除了项目经理之外,还会配置项目辅助管理人员和咨询顾问管理人员等。如果说项目超过了10000人/月这个规模,项目往往会采用纵向切割来进行管理,整个项目会像工厂中产品线生产方式:系统需求;系统设计;配置管理;代码开发;系统测试;文档编写;产品构建等过程,整个项目会根据不同的分工被切割成每个小项目团队,虽然每个团队可能的工作都只是针对于局部,在各自的内部这些工作是相对独立的,但是每个项目又都对其他部分有比较严重的依托,比如系统设计是以项目需求为基础,代码开发是以系统设计为前提,所有的工作序列彼此关联,每个工作都可以独立安排二级甚至三级的项目经理,这样整个项目的组织管理模式也就形成了金字塔的模式,从项目经理到最底下的开发人员形成一个自顶向下的体系结构。这个时候对于项目经理的主要工作也就不能要求事必躬亲,小到一个螺丝钉都要亲自过问,对于这种项目经理的要求更多在于总体协调和整体的掌控上面,他就像一个元帅一般的任务,要的是果断的决策,准确的判断,良好的协调和丰富的管理经验。
实际上大部分的项目经理很难有机会成为如此大规模项目的最高决策者,即便有机会参与的时候,更多都是处在二线或者三线的位置,所能够管理的实际人员也大部分在10人或则20人左右。更多项目经理参与的项目都是中小规模的项目,毕竟中小的项目的数量还是非常巨大,所以有很多的项目经理在从事这种的开发工作。对于项目规模在100人/月的项目对于很多公司来说也算是具备有一定规模的项目,这些项目的人员投入一般都会在10人之上,不会有公司对这种项目采用投入一个人做一百个月的方式。对于10人规模的项目管理对于大部分的项目经理来说可以是一个不小的挑战,因为虽然说项目的规模不能与上述所说的超级项目抗衡,但是项目在整体过程中所做的事情和上述相当,系统需求,系统设计,配置管理,代码开发,系统测试,文档编写,产品构建等都不会缺少,但是在人员配备里面可没有二级或则三级的项目经理,甚至很多时候会有人力资源捉襟见肘的情况,在10人的规模里面可是要包含需求做成人员,项目开发人员,测试人员等。在很多的时候一个人需要充当多个角色,可能在某一个阶段你需要做需求,当需求做完了之后,你又需要投入开发,后期的时候可能需要做项目文档和项目部署,甚至用户培训。所以对于我的直观感觉来说,这种项目对于项目经理的能力要求可能更高,如果要做好这种项目的项目经理,首先最好有比较扎实的技术开发能力和工程背景,这样你的项目估算才能做得比较准确,为后续的开发赢得更多的时间;其次好需要又比较良好的业务知识,因为你可能更多需要做一个用户的接口,所有的需要都需要通过你和客户去沟通,以及最终的定案,所以对于业务的熟悉程度也至关重要;再者需要又良好的计划性,因为这种项目的变数比较多,项目不会像又很长的规划周期,所以如果控制好这些可变因素,已经如何协调内部的有限资源,让资源发挥最大的优势可能成为项目经理每日计划中关键的一部分。
还有不少的项目规模在10人/月左右的项目,这种项目对于很多公司来说属于不痛不痒的项目类型,因为同一个时期内公司可能会有多个这种类型的项目同时在进行中,在这种情况下项目所能够获得的资源就更加有限,往往项目的安置人员也就在5人左右,当然和上面两种情况类是,项目过程的事情还是不可省缺,但是由于项目总体的规模偏小,所以项目的复杂度也比较容易控制,但是这种项目对于项目经理的要求更多在于技术上的要求,因为如果这个时候在安排一个缺乏技术经验的人员做项目经理,那么整个项目组的安排出现一个外行领导四个内行的局面,而且四个内行中可能有一半以上是新人或者初级的开发人员,如果再包括测试等一系列的工作,人员分布上就缺乏合理性。所以这个时候的项目经理往往都是技术过硬的人员担任,这样可以化解人员不足的问题,但是也有一个问题,对于技术优秀的人员都会有一个通病,首先看不上别人的工作结果和质量,另外一方面往往会以自己的标准去衡量别人和给人安排工作,这样对于新人或者初级的开发人员来说,他们的工作就会出现不合理的问题。同时在做估算的时候也比较容易出现轻易的问题,以自己的能力估算项目进度,结果造成项目估算不准确,后期严重加班的问题。所以这种类型的项目经理需要又良好的技术背景,还需要学习项目的实际管理和合理的人员安排,以及如何做好与项目内部成员的友好沟通和关系。
最后一种项目类型就是那种一个人吃饱,全家不额的类型,项目规模非常小,不到1人/月,所以在这种项目的投入人数往往只有一个人或则两个人。当然这个时候也不会要求说项目开发的那一系列的工作都做到位,很多时候都是口头相传,这种项目的结果大部分之要求软件代码和程序能够满足要求。所以这个过程中很多人可能认为只要管好自己就可以了,但是我认为,时间虽然短,但是事情还是需要做足,比如说需求的明确文档化,还有与外界的沟通等一些列过程实际上都可以和正常的模式一样,对自己的工作也需要有一个良好的计划,这个时候对于自己的要求就是一个锻炼的机会,为今后做更大的项目准备好时机。所以这种项目经理的要求实际不抵,如何管理好自己可能会是一个比较大的难题。
项目类型的差异性
项目从规模差异上来说是对一个项目经理的开发能力,管理能力等有不同的要求。但是如果说从项目类型的差异的角度来看,就会对项目经理的一些其他能力又要求,如果说你做的是国内的项目,那么你需要有与客户沟通的良好能力,能够有足够的能力应对客户的各种要求,如何应对客户千奇百怪的要求,如何合理的说“NO.”都是你需要具备的能力。如果说是对日,对欧美的外包服务开发,需要有良好的语言沟通能力,比如说日语,英语。还需要了解不同国家的文化差异等等,这个时候你可能需要充当起桥梁的作用,如何与国外的客户进行沟通和交流,包括有工作安排,技术上等一系列问题的沟通工作。
给自己的定位
项目管理本身就是一个比较复杂的过程,不像行军打战那样,有了一盯一眼的制度就可以管理好项目,因为项目的变数太多,情况迥异,也就没有放之四海而皆准的管理方法。所以对于不同类型的项目来说,我们需要了解项目的特点,在我们有良好的基础准备的前提下,根据自己的能力特点,再结合项目的实际情款来不断调整工作中的方法和内容。
虽然我们很难有一个标准化的管理手册来指导每一个希望做项目经理的人,但是我们可以从别人身上去借鉴各种成功或则失败的经验,特别是别人失败的经验,因为别人的成功可能我们很难克隆,但是我们可以避免别人错误再我们身上重演。
不想当将军的士兵不是好士兵,但是不想当项目经理的程序员未必是坏的程序员。毕竟对于技术领域来说,程序员的最终发展方向项目经理未必是一个最优秀的方向,程序员可以走的道路有很多,可以往架构师,分析师,资深技术人员,咨询师等等。路可能有很多条,而且每一条对于人员的能力要求也都不一样,都有良好的发展机会。所以对于自己能力的判断和分析,认清自己,给自己合理的定位是直观重要。让自己的才华得到发展和认可是今后职业道路上一个关键,自己要才尽其用。
作者:Yice(小余)
出处:http://www.yice800.cn

自己一个人独自回想工作了这么多年,到底给自己留下了什么?如果要给自己找一个答案,或许有两个会在今后很长一段时间一直影响自己的东西,一个是在工作了这么多年让自己明白了我们要担负什么样的责任,另外一件是工作这么多年给自己的颈椎留下了不小的病症。一好一坏或许是这么多年的最大收获。
我们该如何看待责任
时至今日蒙牛老总牛根生说过的那句话还让我记忆犹新,“有德有才,破格重用;有德无才,培养使用;有才无德,限制录用;无德无才,坚决不用。”在我认为德的基本就是这个人的责任心和他的态度。无论是软件行业还是其他的行业,在很多的时候考核一个人是否符合相应的工作岗位,职位技能往往不是最主要的考核标准,而在所有的考核标准中,人的品性和责任态度才是考核的关键。
什么人活得最轻松,不负责人的人。丈夫有责任给妻子一个良好的生活环境;父亲有责任给孩子一个良好的生长环境;孩子有责任赡养自己的父母;员工有责任做好自己的本职工作;所有的这些都是一些基本的责任。如果一个人富有责任心,生活中的这些责任会让自己感觉到沉重的压力,一旦人在这些压力的之下,要想过的轻松舒服就很困难。所以如果你希望自己过得轻松写意,你放下一切的责任态度,做个不负任何责任的人,那么或许你会过得比较轻松,但是你也将成为社会中一个不合格的人。
软件开发本身属于脑力密集型的劳动,所有的一切都依托于人,软件行业本身和其它的制造行业有一个本质性的差异,软件主要依靠人的智慧来进行工作,虽然我们现在看到很多的软件工程管理书籍,无论是大师的作品,还是坊间口声相传的经验之谈,我们都很难解决一个问题,我们很难像制造行业一样,让我们每个程序员写出来的代码都能够像机器加工出来的那样,每一个行代码都能够像一个模子印出来的,很难做到整个团队写出的代码像出至一人之手。所以软件行业的工程管理比其它的工程来说要存在有更大的困难,那么在这些问题困挠之下,整个行业对人的依赖尤为严重,对人的依赖程度也就造成对人本身品德和责任心的要求程度相对要高。
在自己工作这么多年来,感觉最轻松的时候还是刚刚毕业的时候,那个时候是初出校门,作为一个新人进入项目组中,对于自己最大的挑战主要是能不能按时准确的完成自己相应的模块开发工作。后来开始负责项目之后,虽然也在做编码开发工作,但是所担心的和让自己欢欣的事情和开始已经完全不同。如果你希望自己能够能够作为一个合格的项目经理经理,那么你首先需要明确自己的肩上的担子的分量。在我们的工作中,我们每个人必须承担工作的各种责任,如果我们缺失这种看待责任的态度,那么工作最后的结果往往将会以失败告终。
对于自己在这几年的最大一个一个收获就是让自己更明确到责任重于泰山的道理。当然也是这种态度让自己能够坚持着把一个一个项目做下去,做好这些项目。如果要对自己做一个评价,或许在技术上我不敢说自己有十足的优势,但是这些年养成的这种做事情的方法和态度还能值得一提。
在项目中你该承担什么责任
如果你要问项目组中谁的压力最大,一般说来应该是项目经理,当然项目经理所拿的工资相对来说也会比较高,责任和压力本就应该和报酬成正向比例关系。项目经理对下必须对项目团队成员负责,向上必须对公司负责,同时还需要纵向向客户负责,所以项目经理经理就像一个传动轴承,这个项目的运作应该在他驱使之下有效稳定的运转。
项目经理需要考虑项目的成本因素,所有的公司都是以盈利为主要目的,有时候公司会出于其他的因素和目的,对于项目的盈利并没有很严格的要求,但是项目经理要有成本的概念和意识,要有团队的总体成本和利润要有基本的计算方式。同时还需要控制项目的质量和进度,还有些项目还要求有保密意识和其他的相关要求,这些都是项目经理需要向公司负责的地方。
如何组建好一个团队,如何培养团队的成员,让每个团员很够在一个比较良好的环境中工作和学习,能够实现每个人的目标和各自的价值观,把团队建设成什么样类型,和项目经理所采取的方法有直接的关系,有些项目团队组建后团队人员如走马灯似的换,有些团队组建后队员除了编码之后就没有任何的学习机会,如何利用有限的资源和合理的安排,让团队的成员都能够发挥各自的特长,让每个人都能够体现自己的价值,有时候需要替项目组的成员去和公司去争取他们所应该有的福利和报酬,这就是对项目团队和团队成员所要承担的负责。
我们每一个项目最终都将面对我们的客户,有时候我们的客户的要求会让我们很难接受,甚至有时候会让我们团队感觉到很恼火,但是作为一个项目经理需要化解这些对项目团队一个不利的因素,一方面需要避免影响团队的士气,另外一方面需要和客户进行沟通,明确那些要求可行,那些要求不可行,对于不可行的要求需要给一个比较合理的解释,避免由于后期无法完成对客户造成欺骗的行为。同时需要控制项目的整体进度和质量,保证项目最终能够解决客户的为,这些就是对客户负责的一个态度。
如何树立团队的责任意识
对于项目组中项目尽力所承担的责任应该说是最大的,无论项目的成败都和项目经理由直接的关系,所以任何一个项目做项目总结的时候,如果项目成功的话,需要给项目经理记上一功,如果项目失败的话,不论任何原因,棒子首先需要打在项目经理的身上。项目经理需要对项目的得失成败负上完全的责任。
在一个项目团队中,需要有各个不同的角色相互配合才能最终完成项目,需求调研人员,系统分析人员,高级程序员,初级程序员,测试人员,配置管理人员等,虽然项目经理在项目中的责任最大,但是项目中每一个成员都会有相应承担的责任,或许说着这都是一些工作的职责,开发人员需要按照开发中的各种要求进行开发工作,测试人员需要按照测试的要求准备测试文档和数据等,所有的角色都会有各自的工作内容,在实际工作中我们是每一个角色协作来完成项目,有时候由于项目的规模偏小,有些人可能会同时充当多个角色,比如说高级程序员有时候需要同时兼顾系统分析人员,项目经理有时候还需要兼顾测试等等。但是不论规模的大小,项目经理需要非常清晰的意思到每个角色的工作职责,在项目分工中对各种工作要比较清晰,合理化安排。同时也需要让每个工作的人员清楚的知道自己的工作要求和检验的方式,避免含糊性的安排,做到责任清晰。
明确责任的首要是明确工作内容,对于团队成员中需要做到责任均衡,尽量避免能者多劳的问题,工作中进行工作安排时往往容易把工作重点都落到部分能力较强的人员上,这种安排比较容易造成工作天平的倾斜,一旦倾斜的严重,这部分开发人员就比较容易造成由于精力不足造成质量问题。所以项目经理在安排工作中需要有个权衡,如果说存在人员能力不均衡问题,那么在工作安排的时候,需要尽可能抽取重复性的工作,让能力欠缺的人员做这部分工作,同时需要协调他们学习,也需要明确学习的目标和结果。
对于团队中我们需要树立一个责任意识,同时需要有合作精神,在项目开发中整个团队需要彼此合作,有时候一个人的问题有时候会影响到整个团队的质量和进度。我们最终给客户交付的是一个完整的程序,其中任何一个部分出了问题,客户对整个产品的评价都会因此而改观,客户不会说那个模块怎么怎么样,而是说你这个程序怎么怎么样,这就代表说是一个整体的结果,所以项目经理需要在团队中树立起这种整体的责任意识,避免团队中出现个人自管门前雪,不理他人瓦上霜的现象。
你能承受多大责任
你有没有评价过你的项目经理,你有没有对你的项目经理感到无奈和气氛,如果你希望自己能够往项目经理方向发展,在我看来你首先需要考虑的不是你的技术背景,你的管理经验是不是足够,首先要考虑的是你是不是有能够承受那些责任的心态。如果你是项目经理的话,那么你负责管理的团队无论人员多少,他们需要在你的协调下工作,那么你肩上担负的就是那些人的工作结果和评定,还有来自公司的各种压力和考核。这些比单纯写代码要劳心许多。
影响项目成败的因素有很多,但是如果说项目经理缺少责任心,我可以说项目必败无疑。正是因为这样,我才会在此一再强调责任心的问题。
作者:Yice(小余)
出处:http://www.yice800.cn

如果你爱他,那么让他去当项目经理,因为那里会是他事业的天堂;如果你狠他,送他去当项目经理,因为那将是他的地狱。
软 件开发工作应该属于分工比较明确的行业,每一个项目的启动,调研,开发,测试,部署,用户培训和后期维护等一系列的过程都有不同的角色参与其中。在这一系 列的角色中项目经理是最直接的管理者,无疑显得格外的突出和重要。软件项目开发的成功率本身就不高,在众多的失败过程中,由于项目经理在管理上存在的问题 造成项目无法按时交纳,质量不高甚至失败的例子在我看来数不胜数。虽然项目经理的能力并不是项目失败的直接原因,因为影响项目成败的因素有很多,但是如果 一个合格的项目经理,对于项目的整个开发过程来说,如何利用他的经验和能力来有效合理的管理项目进度,从而避免很多无谓的失误,在项目的最终成败中还是占 有关键作用。
对 于很多从事软件开发的人来说,项目经理是他们事业上追求的目标,从初出校园的小牛犊,从最低级的学徒似的初级开发人员,再不断的努力和学习,慢慢得爬到有 经验的中级程序员,再后来到高级程序员,到后来的大牛人才,慢慢开始带领新人,开始接触项目管理上的工作。我想很多人的轨迹都是这么一步一步的过来。在整 个过程中我们彼此都在学习,关于很多的技术方面的知识可以通过网络和书籍进行学习。但是如何做一名项目经理,如何做好一名项目经理,倒缺乏一个系统的学习 框架,包括我自己在内,也是跟随前人身边学习,自己观察,在一次次错误后进行反思后才有所进步。这个话题的文章我考虑了很久后才决定要写出来,在一系列的 文章中结合我自己的项目和我自己身边的项目,希望能够将这些经验与大伙分享,通过讨论,彼此共勉。机会往往是给有所准备的人,不论你现在是否是充当项目经 理的角色,但是如果你有所准备,我想对于你来说机会只是迟早的事情。
项目从规模来说,可以划分微型项目,小型项目,中型项目和大项目,当然还有超大型的项目,对于工数在一人/月(一个中级程序员开发一个月,总计21个工作日)的项目定为微型项目;对于工数在1人/月到10人/月之间的规模称为小型项目;对于工数在10人/月到100人/月之间的规模称为中型项目;如果超过 100人/月的项目称为大型项目;对于我们所讨论的项目管理中,对于超过1000人/月的项目不做讨论,因为一般的公司来说,还是比较少能够遇到中规模的项目。
如 果从类型来说可以简单的划分为产品开发和项目开发,产品的开发一般会有后续定期的产品升级性开发,项目的开发时间跨度也会比较长,对于项目的开发来说,一 般是指为了满足某一特定客户而开发的软件,其开发周期往往会比较紧张,后续的开发主要是针对客户的新功能追加,这种项目的开发往往会划分为几个阶段分步进 行。
如果从合作方式上也可以划分为自主研发和外包开发,甚至还有部分项目使用外驻人员进行项目开发,有时候开发还受到地域性的影响,两地,三地合作开发,国内国外的合作开发,还有甚至多国之间的合作开发。
不 同的项目开发方式都会有不同的问题出现,比如说小型项目和大型项目的人员配备上就不可能一样,外包开发和自主研发的项目计划也不一样,跨地域的合作上的时 间差异和人员的沟通和本公司内部背靠背的模式也不一样。项目中实际可能发生的事情千奇百怪,这些问题绝大部分都需要项目经理来过问,分析和决策。所以说项 目经理或许对于很多人来说将会是地狱,一旦深陷其中,很难有苦尽甘来的那一天。但是如果方法得当,管理手段有效,能够合理的规避风险呢?那你将会感到项目 中的一切对你来说游刃有余,团队中每个人也都能相应发挥自己的特长,也都能从中找到各自的成就感。
生与死只在一线间,好和坏也是如此。希望能够从项目经理的角度来看看项目实际过程中我们会遇到哪些问题,该如何去处理这些问题,通过着一些列的文章能让你对项目的整体过程有更全面的了解,同时也能够让你更清楚项目经理的日常工作和行为职责。
作者:Yice(小余)
出处:http://www.yice800.cn

作者:Michael Howard
原文出处: Strsafe.h: Safer String Handling in C

  在微软公司举行的Microsoft Windows Security Push 活动期间,一批测试者、程序管理经理和普通程序员共同决定要为 C 语言量身定制一套具有较高安全性的字符串处理函数,并且希望这些函数能被 Windows 程序员和微软公司内部的程序员所采用。
简单说来,现有的 C 语言运行时函数实在难以在当今充斥着恶意攻击企图的大环境下立足。这些函数要么在返回值和参数上缺乏一致性,要么隐含着所谓的“截断误差”(truncation errors) 错误,要么无法提供足够强大的功能。坦言之,调用这些函数的代码太容易产生“内存溢出”问题了。
  我们发现,面向 C++ 程序员的类足以应付各种安全处理字符串的编程需要;他们能够选择 MFC 的Cstring 类、ATL 的CComBSTR 类 或者STL 的string 类,等等。然而,经典的 C 语言程序仍然普遍地存在,何况许多人正在把 C++ 当作 “改良的 C 语言” 来用,却把丰富的 C++ 类束之高阁。
  其实只需要添加一行代码,你就能在 C 语言代码中调用安全性良好的 strsafe 系列函数了,详细请参阅:
《Using the Strsafe.h Functions》
这些新函数包含在一个头文件和一个函数库(可选)中,而后两者能在新版的 Platform SDK 中找到。对,就这么简单:

一、墨菲定律

1949年,一位名叫墨菲的空军上尉工程师,认为他的某位同事是个倒霉蛋,不经意间开了句玩笑:“如果一件事情有可能被弄糟,让他去做就一定会弄糟。” 这句话迅速流传,并扩散到世界各地。在流传扩散的过程中,这句笑话逐渐失去它原有的局限性,演变成各种各样的形式,其中一个最通行的形式是:“如果坏事情有可能发生,不管这种可能性多么小,它总会发生,并引起最大可能的损失。” 这就是著名的“墨菲定律”。下面是墨菲定律的一些变种或推论。 人生哲学 1.别试图教猫唱歌,这样不但不会有结果,还会惹猫不高兴? 2.别跟傻瓜吵架,不然旁人会搞不清楚,到底谁是傻瓜? 3.不要以为自己很重要,因为没有你,太阳明天还是一样从东方升上来? 4.笑一笑,明天未必比今天好。 5.好的开始,未必就有好结果;坏的开始,结果往往会更糟。 处世原理 6.你若帮助了一个急需用钱的朋友,他一定会记得你——在他下次急需用钱的时候。 7.有能力的——让他做;没能力的──教他做;做不来的──管理他。 8.你早到了,会议却取消;你准时到,却还要等;迟到,就是迟了。 9.你携伴出游,越不想让人看见,越会遇见熟人。 爱情意义 10.你爱上的人,总以为你爱上他是因为:他使你想起你的老情人。 11.你最后硬着头皮寄出的情书;寄达对方的时间有多长,你反悔的时间就有多长。 生活常识 12.东西越好,越不中用。 13.一种产品保证60天不会出故障,等于保证第61天一定就会坏掉。 14.东西久久都派不上用场,就可以丢掉;东西一丢掉,往往就必须要用它。 15.你丢掉了东西时,最先去找的地方,往往也是可能找到的最后一个地方。 16.你往往会找到不是你正想找的东西。 17.你出去买爆米花的时候,银幕上偏偏就出现了精彩镜头。 18.另一排总是动的比较快;你换到另一排,你原来站的那一排,就开始动的比较快了;你站的越久,越有可能是站错了排。 19.一分钟有多长? 这要看你是蹲在厕所里面,还是等在厕所外面。