Colors output from the WebGL shader vary significantly across platforms.
I have a site that makes heavy use of WebGL shaders. When testing on different platforms, I found that the colors on the WebGL canvas did not always match, in particular the shader output, which uses quite a bit of computation to draw a gradient that represents the sky. Colors look much darker on some Windows machines.
The sky shader is a (very slightly) modified version of this: http://threejs.org/examples/#webgl_shaders_sky
The shader code can be found here: http://threejs.org/examples/js/SkyShader.js
When the page has loaded correctly, the sky shader will output this:
However, on some Windows machines it looks like this:
One clue I have so far is that on (Firefox) Windows there are numerous logs in the console alert:
warning: X3571: pow(f, e) will not work for negative f, use abs(f) or conditionally handle negative values if you expect them
EDIT, I went ahead and followed gman's suggestion and added safe functions, my modified shader is below. He still exhibits the same behavior as before. One thing I notice is that there are several constants defined with large or small values, for example.
const float N = 2.545E25;
Could this be the source of the problem? Is there some kind of problem in terms of floating point? Note that the target machine reports high accuracy.
The complete shader is here:
uniform vec3 sunPosition;
uniform float luminance;
uniform float turbidity;
uniform float reileigh;
uniform float mieCoefficient;
uniform float mieDirectionalG;
varying vec3 vWorldPosition;
// constants for atmospheric scattering
const float e = 2.71828182845904523536028747135266249775724709369995957;
const float pi = 3.141592653589793238462643383279502884197169;
const float n = 1.0003; // refractive index of air
const float N = 2.545E25; // number of molecules per unit volume for air at
// 288.15K and 1013mb (sea level -45 celsius)
const float pn = 0.035; // depolatization factor for standard air
// wavelength of used primaries, according to preetham
const vec3 lambda = vec3(680E-9, 550E-9, 450E-9);
// mie stuff
// K coefficient for the primaries
const vec3 K = vec3(0.686, 0.678, 0.666);
const float v = 4.0;
// optical length at zenith for molecules
const float rayleighZenithLength = 8.4E3;
const float mieZenithLength = 1.25E3;
const vec3 up = vec3(0.0, 1.0, 0.0);
const float EE = 1000.0;
const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;
// 66 arc seconds -> degrees, and the cosine of that
// earth shadow hack
const float cutoffAngle = pi/1.95;
const float steepness = 1.5;
// Safe functions
float spow( const float x, const float y ) {
return pow( abs( x ), y );
}
vec3 spow( const vec3 x, const vec3 y ) {
return pow( abs( x ), y );
}
vec3 ssqrt( const vec3 x ) {
return sqrt( abs( x ) );
}
float slog2( const float x ) {
return log2( abs( x ) );
}
float sacos( const float x ) {
return acos( clamp( x, 0.0, 1.1 ) );
}
vec3 totalRayleigh(vec3 lambda)
{
float nn = n * n - 1.0;
return (8.0 * pi * pi * pi * nn * nn * (6.0 + 3.0 * pn)) / (3.0 * N * spow( lambda, vec3(4.0)) * (6.0 - 7.0 * pn));
}
float rayleighPhase(float cosTheta)
{
return (3.0 / (16.0*pi)) * (1.0 + cosTheta * cosTheta);
}
vec3 totalMie(vec3 lambda, vec3 K, float T)
{
float c = (0.2 * T ) * 10E-18;
vec3 ll = (2.0 * pi) / lambda;
return 0.434 * c * pi * spow( ll, vec3(v - 2.0)) * K;
}
float hgPhase(float cosTheta, float g)
{
return (1.0 / (4.0*pi)) * ((1.0 - g * g) / spow( 1.0 - 2.0 * g * cosTheta + g * g, 1.5));
}
float sunIntensity(float zenithAngleCos)
{
return EE * max(0.0, 1.0 - exp(-((cutoffAngle - sacos(zenithAngleCos))/steepness)));
}
// float logLuminance(vec3 c)
// {
// return log(c.r * 0.2126 + c.g * 0.7152 + c.b * 0.0722);
// }
// Filmic ToneMapping http://filmicgames.com/archives/75
const float A = 0.15;
const float B = 0.50;
const float C = 0.10;
const float D = 0.20;
const float E = 0.02;
const float F = 0.30;
const float W = 1000.0;
vec3 Uncharted2Tonemap(vec3 x)
{
return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}
void main()
{
vec3 cameraPos = vec3( 0.0 );
vec3 sunDirection = normalize(sunPosition.xzy);
float reileighCoefficient = reileigh;
float sunfade = 1.0-clamp(1.0-exp((sunPosition.z/450000.0)),0.0,1.0);
// luminance = 1.0 ;// vWorldPosition.y / 450000. + 0.5; //sunPosition.y / 450000. * 1. + 0.5;
// gl_FragColor = vec4(sunfade, sunfade, sunfade, 1.0);
reileighCoefficient = reileighCoefficient - (1.0* (1.0-sunfade));
float sunE = sunIntensity(dot(sunDirection, up));
// extinction (absorbtion + out scattering)
// rayleigh coefficients
vec3 betaR = totalRayleigh(lambda) * reileighCoefficient;
// mie coefficients
vec3 betaM = totalMie(lambda, K, turbidity) * mieCoefficient;
// optical length
// cutoff angle at 90 to avoid singularity in next formula.
float zenithAngle = sacos(max(0.0, dot(up, normalize(vWorldPosition - cameraPos))));
float denom = (cos(zenithAngle) + 0.15 / spow( 93.885 - ((zenithAngle * 180.0) / pi ), 1.253 ));
float sR = rayleighZenithLength / denom;
float sM = mieZenithLength / denom;
// combined extinction factor
vec3 Fex = exp(-(betaR * sR + betaM * sM));
// in scattering
float cosTheta = dot(normalize(vWorldPosition - cameraPos), sunDirection);
float rPhase = rayleighPhase(cosTheta*0.5+0.5);
vec3 betaRTheta = betaR * rPhase;
float mPhase = hgPhase(cosTheta, mieDirectionalG);
vec3 betaMTheta = betaM * mPhase;
vec3 tmp = sunE * ((betaRTheta + betaMTheta) / (betaR + betaM));
vec3 Lin = spow( tmp * ( 1.0 - Fex ), vec3(1.5));
Lin *= mix(vec3(1.0), ssqrt( tmp * Fex ), clamp( spow( 1.0 - dot( up, sunDirection ), 5.0), 0.0, 1.0 ) );
//nightsky
vec3 L0 = vec3( 0.1 ) * Fex;
// composition + solar disc
//if (cosTheta > sunAngularDiameterCos)
float sundisk = smoothstep(sunAngularDiameterCos,sunAngularDiameterCos+0.00002,cosTheta);
// if (normalize(vWorldPosition - cameraPos).y>0.0)
L0 += (sunE * 19000.0 * Fex)*sundisk;
vec3 whiteScale = 1.0 / Uncharted2Tonemap(vec3(W));
vec3 texColor = (Lin+L0);
texColor *= 0.04 ;
texColor += vec3(0.0,0.001,0.0025)*0.3;
float g_fMaxLuminance = 1.0;
float fLumScaled = 0.1 / luminance;
float fLumCompressed = (fLumScaled * (1.0 + (fLumScaled / (g_fMaxLuminance * g_fMaxLuminance)))) / (1.0 + fLumScaled);
float ExposureBias = fLumCompressed;
vec3 curr = Uncharted2Tonemap( ( slog2( 2.0 / spow( luminance, 4.0 ) ) ) * texColor);
vec3 color = curr*whiteScale;
vec3 retColor = spow( color, vec3( 1.0 / ( 1.2 + ( 1.2 * sunfade ) ) ) );
gl_FragColor.rgb = retColor;
gl_FragColor.a = 1.0;
}
The output file www.webglreport.com for the problem machine is below:
Platform: Win32
Browser User Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0
Context Name: webgl
GL Version: WebGL 1.0
Shading Language Version: WebGL GLSL ES 1.0
Vendor: Mozilla
Renderer: Mozilla
Antialiasing: Available
ANGLE: Yes, D3D9
Major Performance Caveat: Not implemented
Vertex Shader
Max Vertex Attributes: 16
Max Vertex Uniform Vectors: 254
Max Vertex Texture Image Units: 4
Max Varying Vectors: 10
Best float precision: [-2127, 2127] (23)
Rasterizer
Aliased Line Width Range: [1, 1]
Aliased Point Size Range: [1, 256]
Fragment Shader
Max Fragment Uniform Vectors: 221
Max Texture Image Units: 16
float/int precision: highp/highp
Best float precision: [-2127, 2127] (23)
Framebuffer
Max Color Buffers: 1
RGBA Bits: [8, 8, 8, 8]
Depth / Stencil Bits: [24, 8]
Max Render Buffer Size: 4096
Max Viewport Dimensions: [4096, 4096]
Textures
Max Texture Size: 4096
Max Cube Map Texture Size: 4096
Max Combined Texture Image Units: 20
Max Anisotropy: 16
Supported Extensions:
ANGLE_instanced_arrays
EXT_frag_depth
EXT_texture_filter_anisotropic
OES_element_index_uint
OES_standard_derivatives
OES_texture_float
OES_texture_float_linear
OES_texture_half_float
OES_texture_half_float_linear
OES_vertex_array_object
WEBGL_compressed_texture_s3tc
WEBGL_depth_texture
WEBGL_lose_context
MOZ_WEBGL_lose_context
MOZ_WEBGL_compressed_texture_s3tc
MOZ_WEBGL_depth_texture
source to share
I'm only guessing this is a bug in ANGLE .
I tried to display the results in different places in the fragment shader. After some debate I found that this line gave different results on Windows vs OSX
vec3 betaR = totalRayleigh(lambda) * reileighCoefficient;
totalRayleigh
as follows
vec3 totalRayleigh(vec3 lambda) {
return (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) /
(3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn));
}
The one looking at the code is a completely constant compile-time expression. So, I nested it in JavaScript, added it Math.
before pow
and then calculated each of the values x
, y
and z
for the result.
Changing it to just use this result like this
vec3 totalRayleigh(vec3 lambda) {
return vec3(0.000005804542996261093,
0.000013562911419845635,
0.00003026590629238531);
}
Fixed problem. I filed an error
If you're curious, I'm not clear what the problem is. Using the extension WEBGL_debug_shaders
shows the shader passed to the driver. The corresponding function looks the same in both GLSL and HLSL
- GLSL -
vec3 webgl_411896f5d9a657de(in vec3 webgl_f4a871322329f1f){
return ((((8.0 * pow(3.1415927, 3.0)) * pow((pow(1.0003, 2.0) - 1.0), 2.0)) * 6.105) / ((76349999198241540557242368.0 * pow(webgl_f4a871322329f1f, vec3(4.0, 4.0, 4.0))) * 5.7550001));
}
- HLSL -
float3 _webgl_411896f5d9a657de(in float3 _webgl_f4a871322329f1f)
{
{
return ((((8.0 * pow(3.1415927, 3.0)) * pow((pow(1.0003, 2.0) - 1.0), 2.0)) * 6.105) / ((76349999198241541000000000.0 * pow(_webgl_f4a871322329f1f, float3(4.0, 4.0, 4.0))) * 5.7550001));
;
}
}
;
Calling points too
- GLSL -
vec3 webgl_6ecdfd37473d07b4 = (webgl_411896f5d9a657de(vec3(6.8000003e-07, 5.4999998e-07, 4.4999999e-07)) * webgl_6a82dfb3c4d3d759);
- HLSL -
float3 _webgl_6ecdfd37473d07b4 = (_webgl_411896f5d9a657de(float3(6.8000003e-007, 5.4999998e-007, 4.4999999e-007)) * _webgl_6a82dfb3c4d3d759);
One idea, perhaps the HLSL compiler doesn't have enough resolution for tiny values? As with using floats to compute compile-time constants instead of doubles? So, I tried to convert this function to C ++
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
const float pi = 3.141592653589793238462643383279502884197169f;
const float n = 1.0003f; // refractive index of air",
const float N = 2.545E25f; // number of molecules per unit volume for air at",
// 288.15K and 1013mb (sea level -45 celsius)",
const float pn = 0.035f; // depolatization factor for standard air",
float totalRayleigh(float lambda) {
return (8.0f * powf(pi, 3.0f) * powf(powf(n, 2.0f) - 1.0f, 2.0f) * (6.0f + 3.0f * pn)) /
(3.0f * N * powf(lambda, 4.0f) * (6.0f - 7.0f * pn));
}
int main() {
//const vec3 lambda = vec3(680E-9, 550E-9, 450E-9);
printf("%g, %g, %g", totalRayleigh(680E-9), totalRayleigh(550E-9), totalRayleigh(450E-9));
return EXIT_SUCCESS;
}
which prints
5.80703e-06, 1.35687e-05, 3.02789e-05
Including these elements in totalRayleigh
still produces the correct result, so this is not a problem.
OTOH change totalRayleigh
to just return vec3(0,0,0)
gives the wrong blue result.
source to share
Gman's answer above is great, I just add the following if others find it helpful. I also went ahead and replaced the big small constants with pre-calculated values ββwhere possible, for both the rayleigh coefficient and the mie coefficient:
#!/usr/bin/python
import math
# Poor man vector lib
def dot( a, b ):
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def scale( v, n ):
return map( lambda x: x * n, v )
def vpow( v, n ):
return map( lambda x: math.pow( x, n ), v )
# Input constants
n = 1.0003 # refractive index of air
N = 2.545E25 # number of molecules per unit volume for air at
pn = 0.035 # depolatization factor for standard air
lamb = (680E-9, 550E-9, 450E-9)
K = (0.686, 0.678, 0.666)
# Rayleigh
nn = n * n - 1.0;
rayleigh = ( 8.0 * math.pow( math.pi, 3 ) * math.pow( nn, 2 ) * ( 6.0 + 3.0 * pn) ) / (3.0 * N *( 6.0 - 7.0 * pn ) )
print "totalRayleigh = rayleigh / pow( lambda, vec3( 4.0 ) )"
print "const float rayleigh =", rayleigh
rayleigh = scale( vpow( lamb, -4.0 ), rayleigh )
print "Fold in lambda..."
print "const vec3 rayleigh =", rayleigh
# Mie
mie = scale( K, 3.472E-18 * math.pow( math.pi, 3 ) / dot( lamb, lamb ) )
print "totalMie = mie * T"
print "const vec3 mie =", mie
source to share