Obj模型功能完善(物体材质,光照,法线贴图).Cg着色语言+OpenTK+F#实现.
这篇文章给大家讲Obj模型里一些基本功能的完善,包含Cg着色语言,矩阵转换,光照,多重纹理,法线贴图的运用.
在上篇中,我们用GLSL实现了基本的phong光照,这里用Cg着色语言来实现另一钟Blinn-phong光照模型,平常我们说语言只是手段,关键是怎么运用,这个用在如一些高级编程语言上,我们或多或少有不同想法,但是在着色语言上,我认为太对了.因语法都是基于C,C++来的,并且去除很多高级特性,可以说语法都是简单到了差不多了,关键在于他内置的一些传递参数的区别上,下来让我们用Cg着色器语言来完善Obj模型里的基本功能.
我们想在.Net环境中使用Cg着色器语言,首先我们需要安装Cg Toolkit,然后使用封装了Cg Toolkit的cgnet,上面还有Cgnet.OpenTK,针对的是Cgnet在OpenTK环境里的简单封装.然后我们在.net环境引用相关DLL,就可以引用到Cg着色器语言了.主要用法和Cg中差不多,在这先说最简单的顶点差色器与片断着色器,首先是生成一个Cg着色器语言环境,然后在这环境里就可以获取最新可用的着色器配置,然后和执行代码生成对应的着色器语言执行对象,在Cg Toolkit安装中,可以看到里有很多的学习例子,上述过程每个例子基本都存在这过程,虽然是用C++写的,看几次就有印象了,额外说一句,本来我看那些例子还分DXD9,DXD10等,OpenGL就一个,还在想,这是不是太偏向DX了,那想打开一看,Opengl里的初级,高级例子比DXD9,DXD10加起来都多,不知是DX本身自带还是昨的,反正用OpenGL的足够学习如何使用Cg语法了.下面针对Cg里做一个简单的封装.
type CgContext() = let cgContext = CgNet.Context.Create() let vertexParameters = new ParameterDict() let fragmentParameters = new ParameterDict() do CgGL.SetDebugMode(false) cgContext.ParameterSettingMode <- ParameterSettingMode.Deferred member val VertexProgram = Option<Program>.None with get,set member val FragmentProgram = Option<Program>.None with get,set member val VectexProfile = ProfileType.Unknown with get,set member val FragmentProfile = ProfileType.Unknown with get,set member val ErrorMessage = "" with get,set member this.CreateVectexProgram(fileName,programName) = this.VectexProfile <- ProfileClass.Vertex.GetLatestProfile() this.VectexProfile.SetOptimalOptions() let vertexProgram = cgContext.CreateProgramFromFile( ProgramType.Source, fileName, this.VectexProfile, programName, null) this.ErrorMessage <- cgContext.LastListing vertexProgram.Load() this.VertexProgram <- Some vertexProgram member this.CreateFragmentProgram(fileName,programName) = this.FragmentProfile <- ProfileClass.Fragment.GetLatestProfile() this.FragmentProfile.SetOptimalOptions() let fragmentProfile = cgContext.CreateProgramFromFile( ProgramType.Source, fileName, this.FragmentProfile, programName, null) this.ErrorMessage <- cgContext.LastListing fragmentProfile.Load() this.FragmentProgram <- Some fragmentProfile member this.VertexParameter name= if not (vertexParameters.ContainsKey(name)) then vertexParameters.[name] <- this.VertexProgram.Value.GetNamedParameter(name) vertexParameters.[name] member this.FragmentParameter name= if not (fragmentParameters.ContainsKey(name)) then fragmentParameters.[name] <- this.FragmentProgram.Value.GetNamedParameter(name) fragmentParameters.[name] member this.EnableProfile() = if this.VertexProgram.IsSome then this.VertexProgram.Value.Bind() this.VectexProfile.EnableProfile() if this.FragmentProgram.IsSome then this.FragmentProgram.Value.Bind() this.FragmentProfile.EnableProfile() member this.UpdateParameter() = if this.VertexProgram.IsSome then this.VertexProgram.Value.UpdateParameters() if this.FragmentProgram.IsSome then this.FragmentProgram.Value.UpdateParameters() member this.DisableProfile() = if this.VertexProgram.IsSome then this.VertexProgram.Value.DisableProgramProfiles() if this.FragmentProgram.IsSome then this.FragmentProgram.Value.DisableProgramProfiles() member this.Unload() = if this.VertexProgram.IsSome then this.VertexProgram.Value.Dispose() if this.FragmentProgram.IsSome then this.FragmentProgram.Value.Dispose() cgContext.Dispose()
因为是针对上篇中Obj模型的完善,如这里很多代码是直接在原文的基础之上添加,在上文中,我们法线是读的文本,如果没有,则没有,如果要运用光照,则一定需要法线,我们可以自己来计算.原理很简单,在三角形面中,以二条方向不一样的矢量的叉积就是这个面的法向量,但是通常我们要求的是顶点的法向量,因为在Obj模型中,一个顶点会被多个面使用,故我们用简单的方式来处理,取这点所有面的法线平均.下面是主要代码.
1 //和顶点数组同样长的数组,指定,如果这个数组的下标和顶点数组的下标一样, 2 //则这数组里存放的数据就是顶点数组里的顶点的关联面数,所有法线长度. 3 let pIndN = Array.create this.Positions.Count (0.f,Vector3.Zero) 4 //根据索引信息来给对应的顶点,法线,纹理坐标赋值 5 groups |>List.iter (fun p -> 6 p.Faces.ForEach(fun face -> 7 face.Vectexs |> Array.iter(fun vect -> 8 if vect.PositionIndex > 0 then vect.Position <-this.Positions.[vect.PositionIndex-1] 9 if vect.TexcoordIndex > 0 then vect.Texcoord <- this.Texcoords.[vect.TexcoordIndex-1] 10 if vect.NormalIndex > 0 then vect.Normal <- this.Normals.[vect.NormalIndex-1] 11 ) 12 if this.IsAutoNormal && this.Normals.Count < 1 then 13 let faceNormal = 14 let p1 =Vector3.Subtract(face.Vectexs.[1].Position, face.Vectexs.[0].Position) 15 let p2 =Vector3.Subtract(face.Vectexs.[2].Position, face.Vectexs.[1].Position) 16 Vector3.Cross(p1,p2) 17 face.Vectexs |> Array.iter(fun v -> 18 let mutable ind,n = pIndN.[v.PositionIndex - 1] 19 n <- n + faceNormal 20 pIndN.[v.PositionIndex - 1] <- (ind+1.f,n) 21 ) 22 ) 23 let mater = this.Materials.Find(fun m -> m.Name = p.Mtllib) 24 if box(mater) <> null then 25 let mitem = mater.Items.Find(fun i -> i.Name = p.Usemtl) 26 if box(mitem) <> null then 27 p.Material <- mitem 28 p.Path <- this.Path 29 p.IsHaveMaterial <- true 30 ) 31 if this.IsAutoNormal && this.Normals.Count < 1 then 32 groups |>List.iter (fun p -> 33 p.Faces.ForEach(fun face -> 34 face.Vectexs |> Array.iter(fun v -> 35 let ind,n = pIndN.[v.PositionIndex - 1] 36 v.Normal <- Vector3.Normalize(n / ind) 37 v.LinkFace <- int ind 38 ) 39 ) 40 )
针对原来的处理,增加了十几行的代码,先声明一个和顶点一样长的数组,在这里,我们这样定义,这个数组里存放的数据的下标是和顶点数组中对应顶点的下标一样,这样我们就能直接对应顶点与顶点的共面数,共有法线的信息.相当于天然的HashMap.可以去掉平常算法中的比对过程,如let ind,n = pIndN.[v.PositionIndex - 1]可以直接用自己的下标定位到求得的共面信息与法线总和.
有个法向量后,我们来完善另一个地方,我们原来是物体一直是放在原点下的,也就是模型坐标系和世界坐标系是重和的,我们如果移动,翻转物体后,他就需要自己的模型坐标系了,用来表示他自己与世界坐标系的对应关系.如下代码.
1 let mutable m = Matrix4.Identity 2 let mutable inv = Matrix4.Identity 3 let getLazyModelMatrix() = 4 let tr = Matrix4.CreateTranslation(translation) 5 let ro = if rotate = Vector3.Zero then Matrix4.Identity else Matrix4.CreateFromAxisAngle(rotate,rotateAngle) 6 if bFirstRotate then 7 m <- Matrix4.Mult(tr,ro) 8 else 9 m <- Matrix4.Mult(ro,tr) 10 inv <- Matrix4.Invert(m) 11 m,inv 12 member this.IsFirstRotate with get() = bFirstRotate and set(value) = bFirstRotate <- value 13 member this.Translation 14 with get() = translation 15 and set(value) = 16 translation <- value 17 getLazyModelMatrix() |> ignore 18 member this.Rotate 19 with get() = rotate 20 and set(value) = 21 rotate <- value 22 getLazyModelMatrix() |> ignore 23 member this.RotateAngle 24 with get() = rotateAngle 25 and set(value) = 26 rotateAngle <- value 27 getLazyModelMatrix() |> ignore 28 member this.ModelMatrix with get() = m 29 member this.InvertMatrix with get() = inv
增加一个表示旋转与移动的向量,以及旋转的角度与是否先旋转,先旋转还是先移动生成的模型坐标系是不一样的,注意矩阵相乘的顺序,矩阵不满足交换律的,先后顺序的不同一般会得到不同的矩阵,这里因为在Cg中,最好先经过转置,转置后再乘,就变成如下了,如先R后T,则Matrix4.Mult(T,R),这个顺序非常重要,后面的坐标系变换都要用到.这样我们就生成了模型坐标系,这个坐标系的作用就是把以模型坐标系里的坐标变成世界坐标系的坐标,如果我们需要把世界坐标系的坐标变为模型坐标系的,可以直接用上面的模型坐标系的逆矩阵,具体运行过程大家可以查找相关资料,这些只说下,矩阵与逆矩阵相乘等于单位矩阵,就是对角线都是1,别的位置都是0的矩阵,我们一般定义一个矩阵,默认应该都用单元矩阵.
这里先说下,3D的变换过程大致如下,物体的坐标(经模型坐标系变换成)世界坐标(经过视图坐标系变换成)视图模型坐标系(经透视矩阵变换成)屏幕上的坐标(这里说下,Z值并有没消失,被非线性插值到-1,1之间).这个具体过程,大家想了解可以查找相关资料.下面看一段具体代码.
1 GL.Clear (ClearBufferMask.ColorBufferBit ||| ClearBufferMask.DepthBufferBit) 2 //生成一个视图矩阵 3 let mutable v = Matrix4.LookAt(caram.Eye,caram.Target,Vector3.UnitY) 4 //定位物体在世界坐标系的位置 5 model.Translation <- Vector3(5.f,0.f,0.f) 6 model.Rotate <- Vector3(1.0f,0.f,0.f) 7 model.RotateAngle <- float32 (-Math.PI/2.0) 8 //模型矩阵 9 let m = model.ModelMatrix 10 //Cg与HLSL一样,使用是行矩阵,与OpenGL的列矩阵需要转置才能对应. 11 //如果相应矩阵是传给Cg着色器的,则他在进行相关运行前一定要转置,如果在OpenGL本身运算,则不需要. 12 m.Transpose() 13 v.Transpose() 14 //生成模型视图矩阵 15 let mv = Matrix4.Mult(v,m) 16 //启用相关配置 17 cgContext.EnableProfile() 18 //得到我们设置的视图矩阵. 19 let mutable p = Matrix4.Identity 20 GL.GetFloat(GetPName.ProjectionMatrix,&p) 21 p.Transpose() 22 //生成模型视图透视矩阵 23 let mvp = Matrix4.Mult(p,mv) 24 //传递值. 25 cgContext.VertexParameter("mvp").SetMatrix(MatrixToArray1 mvp) 26 //眼睛的位置由世界坐标转换成模型坐标系. 27 let modelEye = Vector3.Transform(caram.Eye,model.InvertMatrix) 28 cgContext.FragmentParameter("eyePosition").Set(modelEye) 29 //灯光的位置由世界坐标转换成模型坐标系 30 let modelLight = Vector3.Transform(lightPosition,model.InvertMatrix) 31 cgContext.FragmentParameter("lightPosition").Set(modelLight) 32 //针对模型的各参数设置值. 33 model.Groups |> List.iteri (fun i p -> 34 if i < 10 then 35 cgContext.FragmentParameter("Ke").Set(p.Material.Emissive) 36 cgContext.FragmentParameter("Ka").Set(p.Material.Ambient) 37 cgContext.FragmentParameter("Kd").Set(p.Material.Diffuse) 38 cgContext.FragmentParameter("Ks").Set(p.Material.Specular) 39 cgContext.FragmentParameter("shininess").Set(p.Material.Shiness) 40 cgContext.FragmentParameter("dtext").SetTexture(p.Material.DiffuseID) 41 cgContext.FragmentParameter("dtext").EnableTexture() 42 cgContext.FragmentParameter("maptext").SetTexture(p.Material.BumpID) 43 cgContext.FragmentParameter("maptext").EnableTexture() 44 cgContext.UpdateParameter() 45 //p.DrawVBO(cgContext,cgContext.FragmentParameter("tt")) 46 p.DrawVBO() 47 cgContext.FragmentParameter("dtext").DisableTexture() 48 cgContext.FragmentParameter("maptext").DisableTexture() 49 )
这段代码里相关的操作我做了比较详细的注释,在这段代码里,我们没看到相关如加载视图矩阵的操作了,以及针对模型操作调用如GL.Translation,GL.Rotate等操作,这些算法全是我们自己来处理并放入我们写的着色器里来操作,操作的顺序就如上面写的物体的坐标变换的顺序一样,注意矩阵相乘的顺序.过程如果反着来,如世界坐标变成模型坐标,则乘以对应矩阵的逆.
需要说明的几点是,在Cg操作中,矩阵的顺序与DX是一样的,都是行矩阵,而Opengl用的列矩阵,那么如果我们相应的矩阵以及操作过后的顺序给Cg,那么需要在取出来时就先做转置的操作,把列矩阵顺序变成行矩阵的排列顺序.在着色器语言操作中,各个顶点用的坐标系一定要是同一个坐标系,要么都是模型坐标系,要么都是世界坐标系,要么都是视角坐标系,如果不同,显示的效果可能会与你要得到的效果天差之别,如在上面,我们设置灯的位置在世界坐标里的(0,5,8)处,可以看到代码位置,我们用模型的逆矩阵求把对应的世界坐标变成了模型坐标,如果不调用这句,后面会有啥结果了,大家可以先想一下.
写到这个,大家一定好奇相应的Cg的顶点着色器与片断着色器的处理了吧.顶点差色器的处理如下:
1 void v_main(float4 position : POSITION, 2 float3 normal : NORMAL, 3 float2 texCoord : TEXCOORD0, 4 out float4 oPosition : POSITION, 5 out float4 objectPos : TEXCOORD0, 6 out float3 oNormal : TEXCOORD1, 7 out float2 oTexCoord : TEXCOORD2, 8 uniform float4x4 modelView, 9 uniform float4x4 mvp) 10 { 11 oPosition = mul(mvp,position); 12 objectPos = position; 13 oNormal = normal; 14 oTexCoord = texCoord; 15 }
顶点差色器我们可以看到后面有一些out,uniform,POSITION,TEXCOORD0的关键词,让我们来解析一下相关参数的功能,前三个float4 position : POSITION,float3 normal : NORMAL,float2 texCoord : TEXCOORD0在类型前面没有关键词,那表示相应数据是Opengl传递给我们的,这个时候后缀很重要,第一个POSITION就表示传递的是当前的顶点,NORMAL与TEXCOORD0同理.那么后面的如out float4 oPosition : POSITION,out float4 objectPos : TEXCOORD0,out float3 oNormal : TEXCOORD1,out float2 oTexCoord : TEXCOORD2. 前面才说,后面的后缀如POSITION这些很重要,指定是传入的数据,那么在这里,后缀就与他单词的意义没有关系了,可以看到这些前面都带一个out,这表示这些数据都是传递给片断着色器的,这些后缀与片断着色器的对应,表示对应的传值关系.最后的uniform float4x4 modelView,uniform float4x4 mvp表示的是我们从应用程序传递过来的数据,在这里我们分别传来一个模型视图矩阵,一个模型视图透视矩阵,如果我们要把所有值都变成在模型视图下的坐标,我们可以用到这值,但是在这,我们都用模型坐标系,所以没用到,和GLSL一样,我们要得到当前顶点的在模型视图透视的位置,也就是我们看到的屏幕位置.前面说了,如果世界坐标没有变成模型坐标,在这里,大家还可以处理一下,得到正确的位置,增加一个传入的模型矩阵,把当前世界坐标系的位置用这矩阵的逆变成模型坐标系.如果这步你还没进行,那么相关数据就到片断着色器中了.
在这里说下,如果大家都用模型视图坐标系,请注意,如果我们设置这个坐标系下,朝向是向着Z方向前看的,就是越远Z值越大.那么视角下的的位置和我们OpenGL的位置是不一样的,这个坐标系和OpenGL的Z值与X轴方向是反的,这样想吧,我们在屋内看门的右边就是我们在屋外看门的左边.我开始全用的是模型视图坐标系,偏偏和DX一样,是向着Z轴向前看(没办法,模型加载很多都是这种方向)就是因为这个地方,一些位置老不对,搞的我好怨念啊,你为毛不和DX一样,用符合人体视角的坐标系.
上在的顶点着色器处理后,就到我们的片断着色器,代码主要过程如下:
1 float3 expand(float3 v) 2 { 3 return (v-0.5) * 2.0; 4 } 5 6 void f_main(float4 position : TEXCOORD0, 7 float3 normal : TEXCOORD1, 8 float2 texCoord : TEXCOORD2, 9 out float4 color : COLOR, 10 uniform float3 globalAmbient, 11 uniform float3 lightColor, 12 uniform float3 lightPosition, 13 uniform float3 eyePosition, 14 uniform float3 Ke, 15 uniform float3 Ka, 16 uniform float3 Kd, 17 uniform float3 Ks, 18 uniform sampler2D dtext, 19 uniform sampler2D maptext, 20 uniform float3 tt, 21 uniform float shininess 22 ) 23 { 24 float3 N = normal; 25 // Compute emissive term 26 float3 emissive = Ke; 27 // Compute ambient term 28 float3 ambient = Ka * globalAmbient; 29 // Compute the diffuse term 30 float3 L = normalize(lightPosition - P); 31 float diffuseLight = max(dot(L, N), 0); 32 float3 diffuse = Kd * lightColor * diffuseLight; 33 // Compute the specular term 34 float3 V = normalize(eyePosition - P); 35 float3 H = normalize(L + V); 36 float specularLight = pow(max(dot(H, N), 0), shininess); 37 if (diffuseLight <= 0) specularLight = 0; 38 float3 specular = Ks * lightColor * specularLight; 39 //float3 tex =lerp(tex2D(maptext, texCoord).xyz,tex2D(dtext, texCoord).xyz,1.0); 40 float3 tex = tex2D(dtext, texCoord).xyz; 41 float3 light = emissive + ambient + diffuse + specular; 42 color.xyz = light * tex; 43 // color.xyz = lerp(light,tex,0.5); 44 // color.xyz = light; 45 color.w = 1; 46 }
在这里,前面的参数也有很多关键词,和前面大部分是一样的,就是在类型没有前缀,后面又带着后缀的,如float4 position : TEXCOORD0, float3 normal : TEXCOORD1,float2 texCoord : TEXCOORD2,这些就是前面顶点着色器传过来的值.别的就out float4 color : COLOR和前面的out float4 oPosition : POSITION一样,都是应用的处理,传递回给OpenGL用,一个对应的顶点位置,一个对应片断处理的颜色.后面的uniform一样,是表示从OpenGL应用程序传递进来的值.这个光照模型的算法称作Blinn-phong,对于上一种光照主要改进在于镜面光照的计算,他计算顶点到光照与顶点到人眼的矢量二者相加的,因为顶点到光照与顶点到人眼的矢量都取的是单元向量,所以他们相加的矢量,就在他们的半角上,所以这种计算方式也叫求半角,然后求与法线的叉积就是我们要求的镜面反射量。
下面我们来说关于法线贴图相关操作,在Cg中,启用多个纹理相对来说比较简单,调用对应有API就能启用,分别是关联纹理,启用纹理,关闭纹理,不需要GLSL那样还需要调用GL.ActiveTexture这种API来指定当前纹理。首先,我直接把法线贴图里的RGB转成法线,然后原来的法线替换法线帖图里的法线,结果嘛,在某个方向,我们发现能得到正常的光照,但是更多的位置查看是错误的结果,如有黑块等等现象。那时因为在纹理里的光照存取的都是模型在某个位置时的值,如果模型经过一些旋转等操作,此时在这个光照已经对应不上了,我们想想一面墙,面对我们时法向量是Z轴,如果把墙转个90度,那时我们来看法向量就是Y轴,好吧,我感觉这个还复杂了说,你直接想,画一个立方体,他的六面法向量各不一样,现在引入一个矩阵,让你六面法向量只需要设置一次,效果如模型矩阵一样,他能让你只设置一种情况下的法向量,外界的改变会反映在这矩阵上,我们要做的只是和这矩阵的操作,而不需要去关注他本身的变换。这个矩阵所对应的坐标系是切线坐标系。下面我给出别人对切线空间比较深刻的说明,希望对大家的理解有帮助。
http://www.opengpu.org/forum.php?mod=viewthread&tid=5169这个里面三楼的回复:
简单地说就是:
1、楼上讲的参数曲面上任一点都有切空间,并且有无数个切空间,其中法线是固定的,它与切平面上任意两条相互垂直的线(副法线与切线)就构成了一个切空间。
2、法线贴图的用的那个切空间,就是指副法线与切线刚好与uv轴重合的那个。
3、用切空间的好处之一是,对某些对称的模型,只用做一半贴图,就可以贴两面,因为几何体在镜像后,对象空间的法线变了,但是切空间里的没变。
根据如http://blog.csdn.net/bonchoix/article/details/8619624里下的这张图,能很好说明切线空间中的U,V如何与模型坐标系的顶点对应上的,注意大部分情况都是模型坐标系,意思是相应的模型坐标的结果通过切线矩阵TNB得到在对应切空间的位置,反过来也可以把要空间里的坐标通过TNB得到模型坐标的结果。与别的坐标系的交互要先通过TNB来操作。
知道算法后,我们就可以求得切线了,和求法线一样,需要先求得顶点的各个切线,然后取平均。下面给出主要代码。
1 p.Faces.ForEach(fun face -> 2 let p10 = face.Vectexs.[1].Position - face.Vectexs.[0].Position 3 let p20 = face.Vectexs.[2].Position - face.Vectexs.[0].Position 4 let t10 = face.Vectexs.[1].Texcoord - face.Vectexs.[0].Texcoord 5 let t20 = face.Vectexs.[2].Texcoord - face.Vectexs.[0].Texcoord 6 let T = (t20.Y * p10 - t10.Y * p20) / (t10.X*t20.Y - t10.Y*t20.X) 7 face.Vectexs |> Array.iter(fun vect -> 8 let mutable ind,n = pIndT.[vect.PositionIndex - 1] 9 n <- n + T 10 pIndT.[vect.PositionIndex - 1] <- (ind+1.f,n) 11 ) 12 ) 13 if p.Material.BumpMap <> "" then 14 p.Faces.ForEach(fun face -> 15 face.Vectexs |> Array.iter(fun v -> 16 let ind,n = pIndT.[v.PositionIndex - 1] 17 v.Tangent <- Vector3.Normalize(n / ind) 18 v.LinkFace <- int ind 19 ) 20 ) 21 )
求得切线后,下一步就是写入内存,因为我们使用的VBO,那如何才能传入切线到着色器中了,有二种方式,一种是不用VBO,改用直接用一个一个画三角形,在Face中指定切线,好吧,我最开始就试的这个,直接卡的换的摄像机都动不上了。那第二种也就是继续用VBO,传入的时候我们把切线当颜色传入,然后在着色器里取出来,顶点着色器主要代码如下:
1 void v_main(float4 position : POSITION, 2 float3 normal : NORMAL, 3 float2 texCoord : TEXCOORD0, 4 float4 tangent : COLOR, 5 out float4 oPosition : POSITION, 6 out float3 objectPos : TEXCOORD0, 7 out float3 oNormal : TEXCOORD1, 8 out float2 oTexCoord : TEXCOORD2, 9 out float3x3 oTNB : TEXCOORD3, 10 //out float3 oeyePosition: TEXCOORD3, 11 //out float3 olightPosition: TEXCOORD4, 12 //uniform float3 eyePosition, 13 //uniform float3 lightPosition, 14 uniform float4x4 mvp) 15 { 16 oPosition = mul(mvp,position); 17 float3 tNormal = normal; 18 float3 tTangent = tangent.xyz; 19 float3 tB = cross(tNormal,tTangent); 20 float3x3 tnb = float3x3(normalize(tTangent),normalize(tB),normalize(tNormal)); 21 oTNB = tnb; 22 oNormal = normal; 23 oTexCoord = texCoord; 24 objectPos = position.xyz; 25 26 //oPosition = mul(mvp,position); 27 //float3 tNormal = normal; 28 //float3 tTangent = tangent.xyz; 29 //float3 tB = cross(tNormal,tTangent); 30 //float3x3 tnb = float3x3(normalize(tTangent),normalize(tB),normalize(tNormal)); 31 //oTNB = tnb; 32 //oTexCoord = texCoord; 33 //objectPos = mul(tnb,position).xyz; 34 }
片断着色器如下:
1 float3 expand(float3 v) 2 { 3 return (v-0.5) * 2.0; 4 } 5 void f_main(float3 position : TEXCOORD0, 6 float3 normal : TEXCOORD1, 7 float2 texCoord : TEXCOORD2, 8 float3x3 tnb : TEXCOORD3, 9 //float3 lightPosition : TEXCOORD3, 10 //float3 eyePosition: TEXCOORD4, 11 out float4 color : COLOR, 12 uniform float3 globalAmbient, 13 uniform float3 lightColor, 14 uniform float3 Ke, 15 uniform float3 Ka, 16 uniform float3 Kd, 17 uniform float3 Ks, 18 uniform float3 lightPosition, 19 uniform float3 eyePosition, 20 uniform sampler2D dtext, 21 uniform sampler2D maptext, 22 uniform float shininess 23 ) 24 { 25 float3 P = position; 26 // float3 normalTex = tex2D(maptext, texCoord).xyz; 27 // float3 N = expand(normalTex);//normalize(normal); // 28 //float3 E = mul(tnb,eyePosition); 29 //float3 Light = mul(tnb,lightPosition); 30 float3 normalTex = tex2D(maptext, texCoord).xyz; 31 float3 N =normalize(mul(inverse(tnb),expand(normalTex))); //normalize(normal); 32 float3 E = eyePosition; 33 float3 Light = lightPosition; 34 // Compute emissive term 35 float3 emissive = Ke; 36 // Compute ambient term 37 float3 ambient = Ka * globalAmbient; 38 // Compute the diffuse term 39 float3 L = normalize(Light - P); 40 float diffuseLight = max(dot(L, N), 0); 41 float3 diffuse = Kd * lightColor * diffuseLight; 42 // Compute the specular term 43 float3 V = normalize(E - P); 44 float3 H = normalize(L + V); 45 float specularLight = pow(max(dot(H, N), 0), shininess); 46 if (diffuseLight <= 0) specularLight = 0; 47 float3 specular = Ks * lightColor * specularLight; 48 //float3 tex =lerp(tex2D(maptext, texCoord).xyz,tex2D(dtext, texCoord).xyz,1.0); 49 float3 tex =tex2D(dtext, texCoord).xyz;//,1.0); 50 float3 light = emissive + ambient + diffuse + specular; 51 color.xyz = light * tex; 52 color.w = 1; 53 }
上面着色器中,分别有一些注释的代码,没注释的是在模型空间运算,注释的是在切线空间运算,效果是一样的。
其实还有第三种方法,不需要在CPU里计算切线,可以启用几何着色器,几何着色器位与顶点着色器与片断着色器之间,在这中间,可以根据顶点着色器中各变量,计算相应结果,如切线,然后传入片断着色器,因我的机器太旧,启用不起来,以后有机会再试。
下面放出对比效果图:
第一张加上法线贴图,第二张没有,这二张是同一个模型,同样的精度,可以看到法线贴图的模型看起来细节要比没有的高不少。
下面放出源代码(记的安装Cg Toolkit):引用DLL 代码 模型文件部分1 模型文件部分2 模型文件部分3 和前面一样,其中EDSF前后左右移动,鼠标右键加移动鼠标控制方向,空格上升,空格在SHIFT下降。
因为模型文件有些大,故分开上传,大家组织好对应目录应该就可以编译过了。
其中大家可以试试求法线或求切线,不按照求平均值的方式,而是在设置面时,对面的每个顶点分别设置时,看看效果,相关部分都有注释。对这部分有兴趣的同学不妨改改相关代码。加了法线贴图后,有时会发现在某些角度看有黑点,现在不知是代码里,相关光照与顶点的计算不在一起的原因还是别的原因,各位如果遇到过这个问题,谢谢指点。