Java-Gaming.org    
Featured games (81)
games approved by the League of Dukes
Games in Showcase (498)
Games in Android Showcase (115)
games submitted by our members
Games in WIP (563)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: [1]
  ignore  |  Print  
  [SOLVED] Specular highlights aren't working  (Read 3470 times)
0 Members and 1 Guest are viewing this topic.
Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Posted 2014-08-08 01:09:43 »

I'm trying to implement specular maps into my shader code, but I have little to no good tutorials on how to calculate it. So far, I've gone on a vague quest to the four corners of the internet, copying the specular component of the tutorials and tried to make some kind of sense from all of it. But so far, this is all I've got. Most of it is incorrect, I should warn you.

pointLight.fs:
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
void main(void) {
   float a = 1.0 / length(light_position.xy - gl_FragCoord.xy);
   vec4 attenuation = vec4(a, a, a, pow(a, 3.0));
   
   vec3 n = normalize((texture2D(s_normal, pass_TextureUV) * 2.0 - 1.0).xyz);
   vec3 l = normalize(vec3((light_position.xy - gl_FragCoord.xy) / resolution.xy, light_position.z));
     
   vec4 c_specular = texture2D(s_specular, pass_TextureUV);
   
   vec3 reflectionVector = reflect(-l, n);
   vec3 surfaceToCamera  = normalize(vec3(pass_Camera_Position) - vec3(pass_Position));
   float cosAngle = max(0.0, dot(surfaceToCamera, reflectionVector));
   float specularComponent = pow(cosAngle, 1);
   
   out_Color = (light_color * max(dot(n, l), 0.0) * attenuation) + specularComponent;
}


pointLight.vs:
1  
2  
3  
4  
5  
6  
7  
8  
9  
void main(void) {
   gl_Position = projMatrix * viewMatrix * modelMatrix * in_Position;
   
   pass_Position = modelMatrix * in_Position;
   pass_Color = in_Color;
   pass_TextureUV = in_TextureUV;
   
   pass_Camera_Position = viewMatrix * vec4(0.0, 0.0, 0.0, 0.0) - modelMatrix * in_Position;
}


Final result with diffuse map (Which is done by a separate shader pass)
MP4 version: http://i.gyazo.com/c41238766703772b7dc634e99b5d3c32.mp4
Click to Play


Ecu doesn't commit to master, master commits to Ecu
Offline SHC
« Reply #1 - Posted 2014-08-08 02:34:46 »

What you are trying to achieve can be done easily by using the phong reflection model.



First, we implement the ambient component. This component specifies the color of the object when it's in no light. Here's the shader code that specifies the ambient component.

1  
vec3 ambient = ambientCoefficient * vColor.rgb * lightIntensity;

Where the terms
ambientCoefficient
values to 0.05 and
lightIntensity
values to 2.0. That's the ambient part of the light. Now, let's get to the diffuse component. This component darkens out the parts that are in complete darkness. Here's the shader code that calculates this component.

1  
2  
3  
4  
5  
6  
7  
8  
// First, we need the surface to light vector
vec3 surfaceToLight = normalize(lightPos - vertPos);

// Now calculate the diffuse coefficient
float diffuseCoefficient = max(0.0, dot(vNormal, surfaceToLight));

// Finally calculate the diffuse component
vec3 diffuse = diffuseCoefficient * vColor.rgb * lightIntensity;

Now, finally we implement the specular component. This is what that gives the shininess on the model. Here's the shader code that does this.

1  
2  
3  
4  
5  
6  
7  
8  
9  
// Specular coefficient is zero if the diffuse coefficient is less than or equal to zero
float specularCoefficient = 0.0;
if(diffuseCoefficient > 0.0)
{
    specularCoefficient = pow(max(0.0, dot(surfaceToLight, reflect(-surfaceToLight, vNormal))), shininess);
}

// The specular component
vec3 specular = specularCoefficient * vec3(1.0, 1.0, 1.0) * lightIntensity;

That said, now we finally compute the fragment color by multiplying all these components.

1  
outColor = vColor + vec4(ambient, 1.0) + vec4(diffuse, 1.0) * vec4(lightColor, 1.0) + vec4(specular, 1.0);

And it simply works. Hope this helps.

Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Reply #2 - Posted 2014-08-08 03:39:05 »

Once again, and amazing, strait to the point answer. Thanks SHC!

Does this look correct?
Click to Play


1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
void main(void) {
   vec3 n = normalize((texture2D(s_normal, pass_TextureUV) * 2.0 - 1.0).xyz);
   vec3 l = normalize(vec3((light_position.xy - gl_FragCoord.xy) / resolution.xy, light_position.z));
   
   vec3 surfaceToLight = normalize(vec3(light_position.x, resolution.y - light_position.y, light_position.z) - vec3(pass_Position));
   float diffuseCoefficient = max(dot(n, l), 0.0);
   
   float specularCoefficient = 0.0;
   float shininess = texture2D(s_specular, pass_TextureUV).r * 255.0;
   if(diffuseCoefficient > 0.0){
      specularCoefficient = pow(max(0.0, dot(surfaceToLight, reflect(-surfaceToLight, n))), shininess);
   }
   
   vec3 specular = vec3(specularCoefficient);
   vec3 diffuse  = vec3(diffuseCoefficient);
   
   float a = 1.0 / length(light_position.xy - gl_FragCoord.xy);
   vec4 attenuation = vec4(a, a, a, pow(a, 3.0));
   
   out_Color = (light_color * vec4(diffuse, 1.0) * attenuation) + vec4(specular, 1.0);
}

Ecu doesn't commit to master, master commits to Ecu
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline SHC
« Reply #3 - Posted 2014-08-08 03:59:35 »

Yeah, it looks perfect.

Online basil_
« Reply #4 - Posted 2014-08-08 08:09:20 »

does it look better when you do
1  
specularCoefficient = pow(max(0.0, dot(surfaceToLight, reflect(surfaceToLight, n))), shininess);

instead of
1  
specularCoefficient = pow(max(0.0, dot(surfaceToLight, reflect(-surfaceToLight, n))), shininess);


to me it looks like diffuse is correct but not the specular reflection.
.. tho', maybe the calculation is correct but the normal map is a bit weird.
Offline SHC
« Reply #5 - Posted 2014-08-08 11:33:38 »

@basil_

That's because of the documentation of reflect glsl function. It takes a incident vector and the normal of the surface, and reflects it appropriately. Here is the syntax.

1  
genType reflect(genType I, genType N);

And
surfaceToLight
is from the surface to the light, but we need the reverse of it, from light to the surface, so we negate that vector so that the direction is reversed.

Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Reply #6 - Posted 2014-08-08 14:33:16 »

If you guys wan't to look more into it, here's the current code:
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
void main(void) {
   vec3 n = normalize((texture2D(s_normal, pass_TextureUV) * 2.0 - 1.0).xyz);
   vec3 l = normalize(vec3((light_position.xy - gl_FragCoord.xy) / resolution.xy, light_position.z));
   
   vec3 surfaceToLight = normalize(vec3(light_position.x, resolution.y - light_position.y, light_position.z) - vec3(pass_Position));
   float diffuseCoefficient = max(dot(n, l), 0.0);
   
   float specularCoefficient = 0.0;
   float shininess = (texture2D(s_specular, pass_TextureUV).r *255.0);
   if(diffuseCoefficient > 0.0){
      specularCoefficient = pow(max(0.0, dot(surfaceToLight, reflect(-surfaceToLight, n))), shininess);
   }
   
   vec3 specular = vec3(specularCoefficient);
   vec3 diffuse  = vec3(diffuseCoefficient);
   
   float a = 1.0 / length(light_position.xy - gl_FragCoord.xy);
   vec4 attenuation = vec4(a, a, a, pow(a, 3.0));
   
   out_Color = (light_color * vec4(diffuse, 0.0) * attenuation) + vec4(specular, 0.0);
}


Just normals and light color:
Click to Play


1  
- + specular, 0.0);

Click to Play

Ecu doesn't commit to master, master commits to Ecu
Online basil_
« Reply #7 - Posted 2014-08-08 20:46:52 »

@basil_

That's because of the documentation of reflect glsl function. It takes a incident vector and the normal of the surface, and reflects it appropriately. Here is the syntax.

ja I know. it just looks not correct to me. maybe the normalmap is funny (inverted).
Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Reply #8 - Posted 2014-08-08 20:51:32 »

Also, I found this.

1  
(-surfaceToLight)

Click to Play


1  
(surfaceToLight)

Click to Play


Code:
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
void main(void) {
   vec3 n = normalize((texture2D(s_normal, pass_TextureUV) * 2.0 - 1.0).xyz);
   vec3 l = normalize(vec3((light_position.xy - gl_FragCoord.xy) / resolution.xy, light_position.z));
   
   vec3 surfaceToLight = normalize(vec3(light_position.x, resolution.y - light_position.y, light_position.z) - vec3(pass_Position));
   float diffuseCoefficient = max(dot(n, l), 0.0);
   
   float specularCoefficient = 0.0;
   float shininess = (texture2D(s_specular, pass_TextureUV).r *255.0);
   if(diffuseCoefficient > 0.0){
      specularCoefficient = pow(max(0.0, dot(surfaceToLight, reflect(-surfaceToLight, n))), 1);
   }
   
   vec3 specular = vec3(specularCoefficient);
   vec3 diffuse  = vec3(diffuseCoefficient);
   
   float a = 1.0 / length(light_position.xy - gl_FragCoord.xy);
   vec4 attenuation = vec4(a, a, a, pow(a, 3.0));
   
   out_Color = (light_color * vec4(diffuse, 0.0) * attenuation) + vec4(specular, 0.0);
}

Ecu doesn't commit to master, master commits to Ecu
Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Reply #9 - Posted 2014-08-08 21:14:31 »

Alright, I've put the box at 0, 0 and got this:

Click to Play


Note, the pass_Position is not transformed by the model matrix.

Ecu doesn't commit to master, master commits to Ecu
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline thedanisaur

Senior Member


Medals: 10
Exp: 1 year



« Reply #10 - Posted 2014-08-11 17:30:37 »

Your math is wrong, you need your specular map:

1  
vec4 specular_highlight = texture2D(specular_map, TexCoord);


This:
1  
specularCoefficient = pow(max(0.0, dot(surfaceToLight, reflect(-surfaceToLight, n))), 1);


Should be:
1  
vec3 Reflection = normalize(((2.0 * n_normal) * n_dot_l) - n_light_dir) + specular_highlight.xyz;


You need to dot your reflection and view vectors together
1  
float r_dot_v = max(0.0, dot(Reflection, n_view_dir));


and then calculate your specular color
1  
vec4 specular_color = Specular * specular_highlight * (pow(r_dot_v, specular_power));

Every village needs an idiot Cool
Offline PandaMoniumHUN

JGO Coder


Medals: 32
Exp: 3 years


White-bearded OGL wizard


« Reply #11 - Posted 2014-08-11 18:41:50 »

Your math is wrong, you need your specular map:
...
His math is correct, GLSL's reflect funtion does all the calculations that you just wrote.

OP, if you're interested here's my code for per-fragment two-sided phong shading that supports both positional and directional lights, up to 5 lights:
multipleLights.vs
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
#version 330 core

uniform mat4 projectionMatrix;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;

layout(location=0) in vec3 in_Position;
layout(location=1) in vec3 in_Normal;
layout(location=2) in vec2 in_TexCoord;

out vec4 fs_Position;
out vec3 fs_Normal;
out vec2 fs_TexCoord;

void main(){
   fs_Position = viewMatrix*modelMatrix*vec4(in_Position, 1.0);
   fs_Normal = normalize(vec3(modelMatrix*vec4(in_Normal, 1.0)));
   fs_TexCoord = vec2(in_TexCoord.x, 1.0-in_TexCoord.y);
   // ^ I flip the texture coordinate's Y value, you might have to skip this and simply set fs_TexCoord to in_TexCoord
  gl_Position = projectionMatrix*viewMatrix*modelMatrix*vec4(in_Position, 1.0);
}

multipleLights.fs
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
#version 330 core

struct Light{
   vec4 position;
   vec3 intensity;
};

struct Material{
   vec3 ka;
   vec3 kd;
   vec3 ks;
   float shininess;
};

uniform Light lights[5];
uniform Material material;

in vec4 fs_Position;
in vec3 fs_Normal;
in vec2 fs_TexCoord;

layout(location=0) out vec4 out_Color;

vec3 phongShading(int index, vec3 normal){
   vec3 s = vec3(0.0);
   if(lights[index].position.w == 0.0)
      s = normalize(vec3(lights[index].position));
   else
      s = normalize(vec3(lights[index].position-fs_Position));
   vec3 v = normalize(vec3(-fs_Position));
   vec3 r = reflect(-s, normal);
   vec3 i = lights[index].intensity;
   return i * (material.ka +
      material.kd * max(dot(s, normal), 0.0) +
      material.ks * pow(max(dot(r, v), 0.0), material.shininess));
}

void main(){
   vec3 color = vec3(0.0);
   if(gl_FrontFacing){
      for(int i = 0; i < 5; i++){
         color += phongShading(i, fs_Normal);
      }
   } else {
      for(int i = 0; i < 5; i++){
         color += phongShading(i, -fs_Normal);
      }
   }
   out_Color = vec4(color, 1.0);
}

If you're interested in lighting and GLSL in general pick up OpenGL 4.0 Shading Language Cookbook, it's a great read and explains all the lighting equations in a very simple way.
Also, if you don't understand something in my shaders just ask me and I'll try to elaborate.

My Blog | Jumpbutton Studio - INOP Programmer
Can't stress enough: Don't start game development until you haven't got the basics of programming down! Pointing
Offline thedanisaur

Senior Member


Medals: 10
Exp: 1 year



« Reply #12 - Posted 2014-08-11 18:54:56 »

Oh cool!

I don't know though the highlights look really blown out to me, but I could be wrong.

Every village needs an idiot Cool
Offline PandaMoniumHUN

JGO Coder


Medals: 32
Exp: 3 years


White-bearded OGL wizard


« Reply #13 - Posted 2014-08-11 19:10:43 »

Don't get me wrong, I didn't say that all of his code is correct, only that that part of the code is.
I've quickly read through it and to me it seems to be okay, but I might be wrong since I've never used specular maps - my above inserted shader code doesn't use them either, only calculates specularity based on the viewer position, the normal, the light position and the shininess - .

My Blog | Jumpbutton Studio - INOP Programmer
Can't stress enough: Don't start game development until you haven't got the basics of programming down! Pointing
Offline thedanisaur

Senior Member


Medals: 10
Exp: 1 year



« Reply #14 - Posted 2014-08-11 19:17:49 »

Ah, yeah I'd post my full shader but I'm not sure it's working correctly as it only highlights things properly at the origin of the world, so I'm not sure if it's the frag shader or a vertex transform problem in the calculation on my vert shader.

Every village needs an idiot Cool
Offline pitbuller
« Reply #15 - Posted 2014-08-11 21:36:34 »

1  
fs_Position = normalize(viewMatrix*modelMatrix*vec4(in_Position, 1.0));


Remove that normalization.
Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Reply #16 - Posted 2014-08-11 21:46:01 »

Your math is wrong, you need your specular map:
...
His math is correct, GLSL's reflect funtion does all the calculations that you just wrote.

OP, if you're interested here's my code for per-fragment two-sided phong shading that supports both positional and directional lights, up to 5 lights:

 -snip-

If you're interested in lighting and GLSL in general pick up OpenGL 4.0 Shading Language Cookbook, it's a great read and explains all the lighting equations in a very simple way.
Also, if you don't understand something in my shaders just ask me and I'll try to elaborate.

Thanks for the reply, I'll look into the book. I do plan to get it. Wink

As for the problem, it still looks pretty bad, and I can't find the answer... Here's my current code, I've made it easier to read
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
void main(void) {
   // Normal from normal-map
  vec3 n = normalize((texture2D(s_normal, pass_TextureUV) * 2.0 - 1.0).xyz);
   // Light normal
  vec3 l = normalize(vec3((light_position.xy - gl_FragCoord.xy) / resolution.xy, light_position.z));
   
   // Surface-to-Light normal
  vec3 surfaceToLight = normalize(vec3(light_position.x, resolution.y - light_position.y, light_position.z));
         
   // Normal map
  vec4 diffuse = vec4(max(dot(n, l), 0.0));
   
   vec4 specular = vec4(0.0);
   // Shininess from specular map (Not being used)
  float shininess = (texture2D(s_specular, pass_TextureUV).r *255.0);
   if(diffuse.r > 0.0){
      // Specular calculation (See vec4 specular)
     specular = vec4(pow(max(dot(reflect(-surfaceToLight, n), surfaceToLight), 0.0), 1));
   }
   
   // Attenuation
  float a = 1.0 / length(light_position.xy - gl_FragCoord.xy);
   vec4 attenuation = vec4(a, a, a, pow(a, 3.0));
   
   //Light color * 'shadow' * attenuation + specular = pixel
  out_Color = light_color * diffuse * attenuation + specular;
}


The vertex shader, after what pitbuller said.
1  
2  
3  
4  
5  
6  
7  
void main(void) {
   gl_Position = projMatrix * viewMatrix * modelMatrix * in_Position;
   
   pass_Position = viewMatrix * modelMatrix * in_Position;
   pass_Color = in_Color;
   pass_TextureUV = in_TextureUV;
}


The specular color seems to be working right, I think it has something to do with some position...
Click to Play

Ecu doesn't commit to master, master commits to Ecu
Offline PandaMoniumHUN

JGO Coder


Medals: 32
Exp: 3 years


White-bearded OGL wizard


« Reply #17 - Posted 2014-08-11 21:57:48 »

1  
fs_Position = normalize(viewMatrix*modelMatrix*vec4(in_Position, 1.0));


Remove that normalization.
Oops, correct. Shouldn't have normalized there. Smiley

My Blog | Jumpbutton Studio - INOP Programmer
Can't stress enough: Don't start game development until you haven't got the basics of programming down! Pointing
Offline thedanisaur

Senior Member


Medals: 10
Exp: 1 year



« Reply #18 - Posted 2014-08-11 22:07:31 »

Yep looking good. I'm willing to bet you're having the same problem I'm having though.

I had believed that my issue came from the vertex position information being passed by openGL as I was using immediate mode to render my geometry. However, it looks like you're passing your vertex information into the shader, which means I don't have an idea for the fix atm.

Every village needs an idiot Cool
Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Reply #19 - Posted 2014-08-11 22:13:00 »

Changed
vec3 surfaceToLight = normalize(vec3(light_position.x, resolution.y - light_position.y, light_position.z));

to
vec3 surfaceToLight = normalize(vec3(light_position.x, resolution.y - light_position.y, light_position.z) - pass_Position.xyz);


I don't know... Looks too perfect to me.
Click to Play

MP4: http://i.gyazo.com/832093fa9a2149eb5bb18dc40cdccf77.mp4

Ecu doesn't commit to master, master commits to Ecu
Online basil_
« Reply #20 - Posted 2014-08-12 13:46:35 »

yes it looks good...

just curious, why not to test the math with a simpler and clearer normal-map ? i mean, with your sample you cannot really tell if the math is right or just the "look". it may just look "right" cos' the esthetics look "right" Wink

first found on google :

this should stick out.

(watch out for Y-flipped maps if you search teh interweb)
Offline SHC
« Reply #21 - Posted 2014-08-12 13:58:42 »

The best thing to test these will be the suzanne model from Blender. It'll be perfect for testing lighting.

Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Reply #22 - Posted 2014-08-12 16:24:35 »

just curious, why not to test the math with a simpler and clearer normal-map ? i mean, with your sample you cannot really tell if the math is right or just the "look". it may just look "right" cos' the esthetics look "right" Wink

Okay, does this look right:
Click to Play

Ecu doesn't commit to master, master commits to Ecu
Online basil_
« Reply #23 - Posted 2014-08-12 17:10:39 »

not yet  Undecided

the normal-map describes something that "sticks out" .. so when the lightsource is on right, the left edge should be darker then the center and the right edge should be brighter.

the side that points towards the lightsource looks correct but not the opposite.

does it make sense to you ? Smiley
Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Reply #24 - Posted 2014-08-12 17:29:30 »

I'm sure the normal map works. I think the thing wrong (if anything) is probably the specular normal.

- +specular;

Click to Play


edit:

Removing the mouse light:
Click to Play

Ecu doesn't commit to master, master commits to Ecu
Online basil_
« Reply #25 - Posted 2014-08-12 19:29:23 »

last image looks good Smiley .. what happens if you try general gray instead of black ?
Offline Ecumene

Junior Member


Medals: 5


Generating memes since 2013!


« Reply #26 - Posted 2014-08-12 19:40:06 »

Turns out, my mouse-light was at the Z position 1. So that may have caused the problem  persecutioncomplex
Click to Play

Ecu doesn't commit to master, master commits to Ecu
Online basil_
« Reply #27 - Posted 2014-08-12 21:24:49 »

that looks nice  Cheesy
Pages: [1]
  ignore  |  Print  
 
 

 

Add your game by posting it in the WIP section,
or publish it in Showcase.

The first screenshot will be displayed as a thumbnail.

radar3301 (12 views)
2014-09-21 23:33:17

BurntPizza (29 views)
2014-09-21 02:42:18

BurntPizza (19 views)
2014-09-21 01:30:30

moogie (20 views)
2014-09-21 00:26:15

UprightPath (27 views)
2014-09-20 20:14:06

BurntPizza (31 views)
2014-09-19 03:14:18

Dwinin (48 views)
2014-09-12 09:08:26

Norakomi (74 views)
2014-09-10 13:57:51

TehJavaDev (102 views)
2014-09-10 06:39:09

Tekkerue (50 views)
2014-09-09 02:24:56
List of Learning Resources
by Longor1996
2014-08-16 10:40:00

List of Learning Resources
by SilverTiger
2014-08-05 19:33:27

Resources for WIP games
by CogWheelz
2014-08-01 16:20:17

Resources for WIP games
by CogWheelz
2014-08-01 16:19:50

List of Learning Resources
by SilverTiger
2014-07-31 16:29:50

List of Learning Resources
by SilverTiger
2014-07-31 16:26:06

List of Learning Resources
by SilverTiger
2014-07-31 11:54:12

HotSpot Options
by dleskov
2014-07-08 01:59:08
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!