Ooops
August 15, 2020, 01:57:52 AM

Author Topic: OpenB3D PBR Shader Demo  (Read 1100 times)

Offline iWasAdam

  • Hero Member
  • *****
  • Posts: 1810
Re: OpenB3D PBR Shader Demo
« Reply #15 on: January 05, 2020, 10:58:31 AM »
Quote
are there no GLSL shader experts here to take a look at my problems?
post the issues and we can always look at it and see what can be done :)

Offline Krischan

  • Full Member
  • ***
  • Posts: 219
    • Krischan's Homepage
Re: OpenB3D PBR Shader Demo
« Reply #16 on: January 05, 2020, 02:59:51 PM »
Ok, the issues more detailed: 8)

Screenshot A+B: enabling the Parallax Occlusion Mapping (Key 5), the effect is only barely noticeable and when you get close to walls there is a massive distortion of the UV coordinates. But I don't know why. I've read a lot about POM and an important issue seems that your UV coordinates must be in the "right" space (tangent?), but I don't understand the examples I've found.

And in general, the spotlight looks strange, too flat compared to the fireball. I'd expect more reflection/depth "feedback" from the light there. And that the spotlight reflections change when I move the camera around. It looks like a pointlight which is only moving along the X/Z axis when I walk around but not with the camera rotation. You know what I mean?

Vertex  Shader:
Code:  (Unknown Language)
  1. #version 130
  2.  
  3. #define NUM_LIGHTS 5
  4.  
  5. // ----------------------------------------------------------------------------
  6. // Constants and Structs
  7. // ----------------------------------------------------------------------------
  8.  
  9. struct FloatArray {
  10.         float Float;
  11. };
  12.  
  13.  
  14. // ----------------------------------------------------------------------------
  15. // Output values to the Fragment Shader
  16. // ----------------------------------------------------------------------------
  17.  
  18. out vec2 Vertex_UV;
  19. out vec3 Vertex_Normal;
  20. out vec4 Vertex_Position;
  21. out vec3 Vertex_Eyevector;
  22.  
  23. out vec3 Vertex_LightDir[NUM_LIGHTS];
  24. out vec3 Vertex_LightColor[NUM_LIGHTS];
  25. out float Vertex_LightRange[NUM_LIGHTS];
  26.  
  27. out vec3 Vertex_AmbientColor;
  28.  
  29.  
  30. // ----------------------------------------------------------------------------
  31. // Attributes from the main program
  32. // ----------------------------------------------------------------------------
  33.  
  34. uniform vec2 texscale;
  35. uniform vec2 texoffset;
  36. uniform float ambFactor;
  37. uniform FloatArray lightradius[NUM_LIGHTS];
  38.  
  39.  
  40. // ----------------------------------------------------------------------------
  41. // Main Vertex Shader
  42. // ----------------------------------------------------------------------------
  43.  
  44. void main()
  45. {
  46.         // Normal, Fragment and UV coordinates
  47.         Vertex_Normal = normalize(gl_NormalMatrix * gl_Normal);
  48.         Vertex_Position = gl_ModelViewMatrix * gl_Vertex;
  49.         Vertex_UV = (gl_MultiTexCoord0.xy * texscale) + texoffset;
  50.        
  51.         // Eye vector
  52.         vec4 ecPosition = gl_ModelViewMatrix * gl_Vertex;
  53.         vec3 ecPosition3 = (vec3(ecPosition)) / ecPosition.w;
  54.         Vertex_Eyevector = -normalize(ecPosition3);
  55.  
  56.         // Light properties
  57.         for (int i = 0; i < NUM_LIGHTS; ++i)
  58.         {
  59.                 Vertex_LightDir[i] = gl_LightSource[i].position.xyz - Vertex_Position.xyz;
  60.                 Vertex_LightColor[i] = gl_LightSource[i].diffuse.rgb;
  61.                 Vertex_LightRange[i] = lightradius[i].Float * lightradius[i].Float;
  62.         }
  63.        
  64.         // Ambient color
  65.         Vertex_AmbientColor = gl_LightModel.ambient.rgb * ambFactor;
  66.  
  67.         gl_Position = ftransform();
  68. }

Fragement Shader:
Code:  (Unknown Language)
  1. #version 130
  2.  
  3. #define NUM_LIGHTS 5
  4.  
  5. // ----------------------------------------------------------------------------
  6. // Constants and Structs
  7. // ----------------------------------------------------------------------------
  8.  
  9. struct FloatArray {
  10.     float Float;
  11. };
  12.  
  13. const float PI = 3.14159265359;
  14. const vec2 PMheight = vec2(0.04,-0.02);
  15.  
  16.  
  17. // ----------------------------------------------------------------------------
  18. // Input Textures from the main program
  19. // ----------------------------------------------------------------------------
  20.  
  21. uniform sampler2D albedoMap;
  22. uniform sampler2D normalMap;
  23. uniform sampler2D roughnessMap;
  24. uniform sampler2D metallicMap;
  25. uniform sampler2D heightMap;
  26. uniform sampler2D aoMap;
  27. uniform sampler2D emissionMap;
  28.  
  29.  
  30. // ----------------------------------------------------------------------------
  31. // Attributes from the Vertex Shader
  32. // ----------------------------------------------------------------------------
  33.  
  34. in vec2 Vertex_UV;
  35. in vec3 Vertex_Normal;
  36. in vec4 Vertex_Position;
  37. in vec3 Vertex_Eyevector;
  38. in vec3 Vertex_LightDir[NUM_LIGHTS];
  39. in vec3 Vertex_LightColor[NUM_LIGHTS];
  40. in float Vertex_LightRange[NUM_LIGHTS];
  41. in vec3 Vertex_AmbientColor;
  42.  
  43. out vec4 FragColor;
  44.  
  45.  
  46. // ----------------------------------------------------------------------------
  47. // Attributes from the main program
  48. // ----------------------------------------------------------------------------
  49.  
  50. uniform float levelscale;
  51. uniform float gamma;
  52.  
  53. uniform float fogStart;
  54. uniform float fogRange;
  55. uniform float fogDensity;
  56. uniform vec3 fogColor;
  57.  
  58. uniform float AttA;
  59. uniform float AttB;
  60. uniform float flicker;
  61. uniform FloatArray lightradius[NUM_LIGHTS];
  62.  
  63. uniform int isMetal;
  64.  
  65. // ----------------------------------------------------------------------------
  66.  
  67. uniform int flagDM;
  68. uniform int flagFG;
  69. uniform int flagPB;
  70. uniform int flagTM;
  71. uniform int flagTL;
  72.  
  73. // ----------------------------------------------------------------------------
  74.  
  75. uniform int texAL;
  76. uniform int texNM;
  77. uniform int texRO;
  78. uniform int texME;
  79. uniform int texHM;
  80. uniform int texAO;
  81. uniform int texEM;
  82.  
  83.  
  84. // ----------------------------------------------------------------------------
  85. // Normal functions
  86. // ----------------------------------------------------------------------------
  87.  
  88. mat3 cotangent_frame(vec3 N, vec3 p, vec2 uv)
  89. {
  90.         vec3 dp1 = dFdx(p);
  91.         vec3 dp2 = dFdy(p);
  92.         vec2 duv1 = dFdx(uv);
  93.         vec2 duv2 = dFdy(uv);
  94.  
  95.         vec3 dp2perp = cross(dp2, N);
  96.         vec3 dp1perp = cross(N, dp1);
  97.         vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
  98.         vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
  99.  
  100.         float invmax = inversesqrt(max(dot(T, T), dot(B, B)));
  101.         return mat3(T * invmax, B * invmax, N);
  102. }
  103.  
  104. // ----------------------------------------------------------------------------
  105.  
  106. vec3 perturb_normal(vec3 N, vec3 V, vec3 map, vec2 texcoord)
  107. {
  108.         map = map * 255.0 / 127.0 - 128.0 / 127.0;
  109.         mat3 TBN = cotangent_frame(N, -V, texcoord);
  110.         return normalize(TBN * map);
  111. }
  112.  
  113.  
  114. // ----------------------------------------------------------------------------
  115. // Tonemapping functions
  116. // ----------------------------------------------------------------------------
  117.  
  118. vec3 ToneMapFilmic(vec3 color)
  119. {
  120.         vec4 vh = vec4(color, gamma);
  121.         vec4 va = 1.425 * vh + 0.05;
  122.         vec4 vf = (vh * va + 0.004) / (vh * (va + 0.55) + 0.0491) - 0.0821;
  123.         return vf.rgb / vf.www;
  124. }
  125.  
  126. // ----------------------------------------------------------------------------
  127.  
  128. vec3 ToneMapSimple(vec3 color)
  129. {
  130.         return pow(color / (color + vec3(1.0)), vec3(1.0 / gamma));
  131.        
  132. }
  133.        
  134. // ----------------------------------------------------------------------------
  135.  
  136. vec3 ToneMapExposure(vec3 color)
  137. {
  138.         color = exp(-1.0 / ( 2.72 * color + 0.15 ));
  139.         color = pow(color, vec3(1. / gamma));
  140.         return color;
  141. }
  142.  
  143. // ----------------------------------------------------------------------------
  144.  
  145. vec3 ToneMapPBR(vec3 color)
  146. {
  147.         // HDR tonemapping
  148.         color = color / (color + vec3(1.0));
  149.         // gamma correct
  150.         color = pow(color, vec3(1.0 / gamma));
  151.        
  152.         return color;
  153. }
  154.  
  155. // ----------------------------------------------------------------------------
  156.  
  157. vec3 Uncharted(vec3 x)
  158. {
  159.         float A = 0.15;
  160.         float B = 0.50;
  161.         float C = 0.10;
  162.         float D = 0.20;
  163.         float E = 0.02;
  164.         float F = 0.30;
  165.        
  166.         return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
  167. }
  168.  
  169. // ----------------------------------------------------------------------------
  170.  
  171. vec3 ToneMapUncharted(vec3 color)
  172. {
  173.         color = Uncharted(color * 4.5) * (1.0 / Uncharted(vec3(11.2)));
  174.         color = pow(color, vec3(1.0 / gamma));
  175.         return color;
  176.  
  177. }
  178.  
  179. // ----------------------------------------------------------------------------
  180.  
  181. vec3 ToneMapSCurve(vec3 x)
  182. {
  183.         //x = pow(x, vec3(1.0 / 2.2));
  184.        
  185.         float a = 2.51f;
  186.         float b = 0.03f;
  187.         float c = 2.43f;
  188.         float d = 0.59f;
  189.         float e = 0.14f;
  190.         return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
  191. }
  192.  
  193.  
  194. // ----------------------------------------------------------------------------
  195. // PBR functions
  196. // ----------------------------------------------------------------------------
  197.  
  198. float DistributionGGX(vec3 N, vec3 H, float roughness)
  199. {
  200.         float a = roughness * roughness;
  201.         float a2 = a * a;
  202.         float NdotH = max(dot(N, H), 0.0);
  203.         float NdotH2 = NdotH * NdotH;
  204.  
  205.         float nom = a2;
  206.         float denom = (NdotH2 * (a2 - 1.0) + 1.0);
  207.         denom = PI * denom * denom;
  208.  
  209.         return nom / denom;
  210. }
  211.  
  212. // ----------------------------------------------------------------------------
  213.  
  214. float GeometrySchlickGGX(float NdotV, float roughness)
  215. {
  216.         float r = (roughness + 1.0);
  217.         float k = (r * r) / 8.0;
  218.  
  219.         float nom = NdotV;
  220.         float denom = NdotV * (1.0 - k) + k;
  221.  
  222.         return nom / denom;
  223. }
  224.  
  225. // ----------------------------------------------------------------------------
  226.  
  227. float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
  228. {
  229.         float NdotV = max(dot(N, V), 0.0);
  230.         float NdotL = max(dot(N, L), 0.0);
  231.         float ggx2 = GeometrySchlickGGX(NdotV, roughness);
  232.         float ggx1 = GeometrySchlickGGX(NdotL, roughness);
  233.  
  234.         return ggx1 * ggx2;
  235. }
  236.  
  237. // ----------------------------------------------------------------------------
  238.  
  239. vec3 fresnelSchlick(float cosTheta, vec3 F0)
  240. {
  241.         if(cosTheta > 1.0)
  242.                 cosTheta = 1.0;
  243.         float p = pow(1.0 - cosTheta,5.0);
  244.         return F0 + (1.0 - F0) * p;
  245. }
  246.  
  247.  
  248. // ----------------------------------------------------------------------------
  249. // Parallax Occlusion Mapping
  250. // ----------------------------------------------------------------------------
  251.  
  252. vec2 ParallaxOcclusionMapping(vec2 texCoords, vec3 viewDir)
  253. {
  254.         // number of depth layers
  255.         const vec2 heightScale = vec2(0.00625,-0.00625);
  256.         const float minLayers = 8;
  257.         const float maxLayers = 32;
  258.         float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
  259.        
  260.         // calculate the size of each layer
  261.         float layerDepth = 1.0 / numLayers;
  262.        
  263.         // depth of current layer
  264.         float currentLayerDepth = 0.0;
  265.        
  266.         // the amount to shift the texture coordinates per layer (from vector P)
  267.         vec2 P = viewDir.xy / viewDir.z * heightScale;
  268.         vec2 deltaTexCoords = P / numLayers;
  269.  
  270.         // get initial values
  271.         vec2  currentTexCoords = texCoords;
  272.         float currentDepthMapValue = texture(heightMap, currentTexCoords).r;
  273.      
  274.         while(currentLayerDepth < currentDepthMapValue)
  275.         {
  276.                 // shift texture coordinates along direction of P
  277.                 currentTexCoords -= deltaTexCoords;
  278.                 // get depthmap value at current texture coordinates
  279.                 currentDepthMapValue = texture(heightMap, currentTexCoords).r;  
  280.                 // get depth of next layer
  281.                 currentLayerDepth += layerDepth;  
  282.         }
  283.    
  284.         // get texture coordinates before collision (reverse operations)
  285.         vec2 prevTexCoords = currentTexCoords + deltaTexCoords;
  286.  
  287.         // get depth after and before collision for linear interpolation
  288.         float afterDepth = currentDepthMapValue - currentLayerDepth;
  289.         float beforeDepth = texture(heightMap, prevTexCoords).r - currentLayerDepth + layerDepth;
  290.  
  291.         // interpolation of texture coordinates
  292.         float weight = afterDepth / (afterDepth - beforeDepth);
  293.         vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);
  294.  
  295.         return finalTexCoords;
  296. }
  297.  
  298.  
  299. // ----------------------------------------------------------------------------
  300. // Light functions
  301. // ----------------------------------------------------------------------------
  302.  
  303. float LinearizeDepth(float depth) // Note that this ranges from [0,1] instead of up to 'far plane distance' since we divide by 'far'
  304. {
  305.         float near = 0.1;
  306.         float far = fogRange*1.0/levelscale;
  307.         float z = depth * 2.0 - 1.0; // Back to NDC
  308.         return (2.0 * near) / (far + near - z * (far - near)); 
  309. }
  310.  
  311. // ----------------------------------------------------------------------------
  312.  
  313. float CalcAtt(float distance)
  314. {
  315.         return 1.0 / (1.0 + AttA * distance + AttB * distance * distance);
  316. }
  317.  
  318. // ----------------------------------------------------------------------------
  319.  
  320. float spotlight(int i, float attenuation, vec3 L, float intensity)
  321. {
  322.         float clampedCosine = max(0.0, 1.0 * dot(-L, gl_LightSource[i].spotDirection));
  323.         attenuation = attenuation * pow(clampedCosine, gl_LightSource[i].spotExponent*intensity);
  324.  
  325.         return attenuation;
  326. }
  327.  
  328.  
  329. // ----------------------------------------------------------------------------
  330. // Main Fragment Shader
  331. // ----------------------------------------------------------------------------
  332.  
  333. void main()
  334. {
  335.         vec2 uv = Vertex_UV;
  336.                
  337.         // Parallax Mapping
  338.         if(texHM > 0){uv = ParallaxOcclusionMapping(uv, normalize(Vertex_Eyevector.xyz));}
  339.        
  340.         // 1. Albedo Texture
  341.         vec4 albedo = vec4(0.5, 0.5, 0.5, 1.0);
  342.         if(texAL > 0){albedo = texture(albedoMap, uv);}
  343.        
  344.         // 2. Normalmap Texture
  345.         vec3 nrm = Vertex_Normal;
  346.         if(texNM > 0){nrm = texture(normalMap, uv).rgb;}
  347.                
  348.         // 3. Roughness Texture
  349.         float roughness = 0.7;
  350.         if(texRO > 0){roughness = texture(roughnessMap, uv).r;}
  351.  
  352.         // 4. Metallic Texture
  353.         float metallic = 0.2;
  354.         if(texME > 0){metallic = texture(metallicMap, uv).r;}
  355.  
  356.         // 5. Ambient Occlusion Texture
  357.         float ao = 1.0;
  358.         if(texAO > 0){ao = texture(aoMap, uv).r;}
  359.                
  360.         // 6. Emissive Texture
  361.         vec3 emission = vec3(0.0);
  362.         if(texEM > 0){emission = texture(emissionMap, uv).rgb * (1.0 + (flicker / 2.0)) * 4.0;}
  363.  
  364.         vec3 Lo = vec3(0.0);
  365.        
  366.         // ambient and emission lighting
  367.         vec3 color = (emission + Vertex_AmbientColor) * albedo.rgb;
  368.         vec3 r = vec3(0.0);
  369.        
  370.         // PBR active
  371.         if(flagPB > 0)
  372.         {
  373.        
  374.                 vec3 N = Vertex_Normal.xyz;
  375.                 vec3 V = normalize(Vertex_Eyevector.xyz);
  376.                 vec3 PN = perturb_normal(N, V, nrm, uv);
  377.  
  378.                 // calculate reflectance at normal incidence; if dia-electric (like plastic) use F0
  379.                 // of 0.04 and if it's a metal, use the albedo color as F0 (metallic workflow)    
  380.                 vec3 F0 = vec3(0.04);
  381.                 F0 = mix(F0, albedo.rgb, metallic);
  382.                 if(isMetal > 0){F0 = albedo.rgb;}
  383.                
  384.                 for(int i = 0; i < NUM_LIGHTS; ++i)
  385.                 {
  386.                         // calculate per-light radiance
  387.                         vec3 L = normalize(Vertex_LightDir[i]);
  388.                         vec3 X = normalize(gl_LightSource[0].spotDirection.xyz);
  389.                        
  390.                        
  391.                         vec3 H = normalize(V + L);
  392.                         float distance = length(Vertex_LightDir[i]) * 1.0 / levelscale;
  393.                         float attenuation = CalcAtt(distance) * Vertex_LightRange[i];
  394.                        
  395.                         // first light = player spotlight
  396.                         if(i == 0 && flagTL == 1)
  397.                         {
  398.                                 attenuation = spotlight(0, attenuation, L, 1.0);
  399.                                 attenuation = pow(attenuation / (attenuation + 1.0 + flicker), 1.0 / gamma) * (2.0 - 2.0 / Vertex_LightRange[i]);
  400.                         }
  401.                        
  402.                         if(i == 0 && flagTL == 0)
  403.                         {
  404.                                 attenuation = 1.0 / (1.0 + 0.0 * distance + 0.5 * distance * distance);
  405.                         }
  406.                        
  407.                         // light color (scaled)
  408.                         vec3 radiance = Vertex_LightColor[i] * attenuation;
  409.                                
  410.                         // Cook-Torrance BRDF
  411.                         float NDF = DistributionGGX(PN, H, roughness);
  412.                         float G = GeometrySmith(PN, V, L, roughness);
  413.                         vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
  414.                    
  415.                         vec3 nominator = NDF * G * F;
  416.                         float denominator = 2 * max(dot(PN, V), 0.0) * max(dot(PN, L), 0.0) + 0.001; // 0.001 to prevent divide by zero.
  417.                         vec3 specular = nominator / denominator;
  418.                
  419.                         // kS is equal to Fresnel
  420.                         vec3 kS = F;
  421.                         // for energy conservation, the diffuse and specular light can't
  422.                         // be above 1.0 (unless the surface emits light); to preserve this
  423.                         // relationship the diffuse component (kD) should equal 1.0 - kS.
  424.                         vec3 kD = clamp(vec3(1.0) - kS, 0.0, 1.0);
  425.                         // multiply kD by the inverse metalness such that only non-metals
  426.                         // have diffuse lighting, or a linear blend if partly metal (pure metals
  427.                         // have no diffuse light).
  428.                         kD *= 1.0 - metallic;
  429.                        
  430.        
  431.                         // non-player light: check backface lighting
  432.                         float NdotL = 1.0;
  433.                         if(i > 0)
  434.                         {
  435.                                 float NdotL = max(dot(PN, L), 0.0);
  436.                                 if(NdotL > 0.0)
  437.                                 {
  438.                                         Lo += (kD * albedo.rgb / PI + specular) * radiance * NdotL * attenuation;
  439.                                         r += attenuation;
  440.  
  441.                                 }
  442.                         }
  443.                                 // player light: simpler light equotation
  444.                         else
  445.                         {
  446.                                 Lo += Lo += (albedo.rgb / PI + specular) * radiance * attenuation;
  447.                                 r += attenuation;
  448.                         }
  449.  
  450.                 }
  451.         }
  452.        
  453.         // PBR off
  454.         else
  455.         {
  456.                 for(int i = 0; i < NUM_LIGHTS; ++i)
  457.                 {
  458.                         vec3 L = normalize(Vertex_LightDir[i]);
  459.                         vec3 N = Vertex_Normal.xyz;
  460.                         float NdotL = max(dot(N, L), 0.0);
  461.  
  462.                         float distance = length(Vertex_LightDir[i]) * 1.0 / levelscale;
  463.                         float attenuation = CalcAtt(distance) * Vertex_LightRange[i];
  464.  
  465.                         // first light = player spotlight
  466.                         if(i == 0 && flagTL == 1)
  467.                         {
  468.                                 attenuation = spotlight(0, attenuation, L, 2.0);
  469.                                 attenuation = pow(attenuation / (attenuation + 1.0 + flicker), 1.0 / gamma) * (2.0 - 2.0 / Vertex_LightRange[i]);
  470.                         }
  471.                        
  472.                         if(i == 0 && flagTL == 0)
  473.                         {
  474.                                 attenuation = 1.0 / (1.0 + 0.0 * distance + 0.5 * distance * distance);
  475.                         }                              
  476.  
  477.                         if(NdotL > 0.0)
  478.                         {
  479.                                 Lo += albedo.rgb * Vertex_LightColor[i] * attenuation * NdotL;
  480.                                 r += attenuation;
  481.                         }
  482.  
  483.                 }
  484.         }
  485.        
  486.         // put it all together
  487.         color += (color + Lo) * ao;
  488.        
  489.         // Tonemapping
  490.         if(flagTM == 1){color = ToneMapFilmic(color);}
  491.         if(flagTM == 2){color = ToneMapSimple(color);}
  492.         if(flagTM == 3){color = ToneMapExposure(color);}
  493.         if(flagTM == 4){color = ToneMapPBR(color);}
  494.         if(flagTM == 5){color = ToneMapUncharted(color);}
  495.         if(flagTM == 6){color = ToneMapSCurve(color);}
  496.        
  497.         // fog
  498.         if(flagFG > 0)
  499.         {
  500.                 float plane;
  501.                 plane = length(Vertex_Position);   // range-based (radial), flat would be plane = abs(position.z)
  502.                 float fogFactor = clamp((fogRange - plane) / (fogRange - fogStart), 0.0, 1.0);
  503.                
  504.                 // distant lights shine through the fog a little bit
  505.                 vec3 fogFactorX = max(clamp(r, 0.0, 0.5), vec3(fogFactor)) * (1.0 - fogDensity);
  506.                
  507.                 color = (fogColor * (1.0 - fogFactorX) + (color.rgb * fogFactorX));
  508.  
  509.         }
  510.        
  511.         // simple depthmap
  512.         if(flagDM > 0)
  513.         {
  514.                 float depth = 1.0 - LinearizeDepth(gl_FragCoord.z);
  515.                 color.rgb = vec3(depth);
  516.         }
  517.        
  518.         FragColor = vec4(color, albedo.a);
  519.  
  520. }
Kind regards
Krischan

Windows 10 Pro | i7 9700K@ 3.6GHz | RTX 2080 8GB]
My Blitzbasic Archive | Extrasolar Project | My Github projects

 

SimplePortal 2.3.6 © 2008-2014, SimplePortal