3
\$\begingroup\$

I always have problems with shaders, while my partners avoid using custom shaders because it always causes problems.

  1. When I write shaders I have to test it in many devices and optimize every time. In addition to testing, each shader should be tested alone. We do not have enough time to test this way.

  2. The result that can be seen in the engine is different from the built version.

  3. In most cases it reduces speed; android phones are very weak and speed is important parameter for us.

We decide to drop custom shaders, but I don't like this decision because efforts are wasted.

So we have to use animated sprites instead of a custom shader but it isn't dynamic. I need way to bake the shaders before building the game.

One solution is pre-rendering my shaders; this means after finishing setting the shader details, generate a sprite sheet or store pixels from the shader. How can I do this?

But it's a ridiculous idea: why use sprites instead of shaders?

So how can I prevent this headache with shaders and have same result in game engine and output?

For example these shaders work on PC but doesn't work on mobile:

Example 1:

https://www.shadertoy.com/view/4dSfDK

image

//Converted by ShaderMan
//https://github.com/smkplus/ShaderMan
Shader"ShaderMan/Space"{
Properties{
_iMouse("iMouse",Vector) = (1,1,1,1)
}
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata{
fixed4 vertex : POSITION;
fixed2 uv : TEXCOORD0;
};
fixed4 _iMouse;
sampler2D _MainTex;

struct v2f
{
fixed2 uv : TEXCOORD0;
fixed4 vertex : SV_POSITION;
fixed4 screenCoord : TEXCOORD1;
};

v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.screenCoord.xy = ComputeScreenPos(o.vertex);
return o;
}
#define M_PI 3.14
#define M_TWO_PI (2.0 * M_PI)

fixed rand(fixed2 n) {
return frac(sin(dot(n, fixed2(12.9898,12.1414))) * 83758.5453);
}

fixed noise(fixed2 n) {
const fixed2 d = fixed2(0.0, 1.0);
fixed2 b = floor(n);
fixed2 f = smoothstep(fixed2(0.0,0.0), fixed2(1.0,1.0), frac(n));
return lerp(lerp(rand(b), rand(b + d.yx), f.x), lerp(rand(b + d.xy), rand(b + d.yy), f.x), f.y);
}

fixed3 ramp(fixed t) {
return t <= .5 ? fixed3( 1. - t * 1.4, .2, 1.05 ) / t : fixed3( .3 * (1. - t) * 2., .2, 1.05 ) / t;
}
fixed2 polarMap(fixed2 uv, fixed shift, fixed inner) {
uv = fixed2(0.5,0.5) - uv;
fixed px = 1.0 - frac(atan2( uv.x,uv.y) / 6.28 + 0.25) + shift;
fixed py = (sqrt(uv.x * uv.x + uv.y * uv.y) * (1.0 + inner * 2.0) - inner) * 2.0;

return fixed2(px, py);
}
fixed fire(fixed2 n) {
return noise(n) + noise(n * 2.1) * .6 + noise(n * 5.4) * .42;
}

fixed shade(fixed2 uv, fixed t) {
uv.x += uv.y < .5 ? 23.0 + t * .035 : -11.0 + t * .03;    
uv.y = abs(uv.y - .5);
uv.x *= 35.0;

fixed q = fire(uv - t * .013) / 2.0;
fixed2 r = fixed2(fire(uv + q / 2.0 + t - uv.x - uv.y), fire(uv + q - t));

return pow((r.y + r.y) * max(.0, uv.y) + .1, 4.0);
}

fixed3 color(fixed grad) {

grad =sqrt( grad);
fixed3 color = fixed3(1.0 / (pow(fixed3(0.5, 0.0, .1) + 2.61, fixed3(2.0,2.0,2.0))));
fixed3 color2 = color;
color = ramp(grad);
color /= (0.1+ max(fixed3(0,0,0)+_iMouse.x, color));
return color;

}

fixed4 frag(v2f i) : SV_Target{
fixed t = _Time.y;
fixed2 uv = i.uv;
fixed ff = 1.0 - uv.y;
fixed2 uv2 = uv;
uv2.y = 1.0 - uv2.y;
uv = polarMap(uv, 1.3, _iMouse.y);
uv2 = polarMap(uv2, 1.9, _iMouse.y);

fixed3 c1 = color(shade(uv, t)) * ff;
fixed3 c2 = color(shade(uv2, t)) * (1.0 - ff);
fixed3 col = c1+c2;
return  fixed4(col,1)*_iMouse.z;
}
ENDCG
}
}
Fallback "Sprites/Default"
}

Example 2:

https://www.shadertoy.com/view/4ttGWM

fire

//Converted by ShaderMan
//https://github.com/smkplus/ShaderMan
Shader"ShaderMan/Fire"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
struct appdata{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}

fixed rand(fixed2 n) {
    return frac(sin(cos(dot(n, fixed2(12.9898,12.1414)))) * 83758.5453);
}

fixed noise(fixed2 n) {
    const fixed2 d = fixed2(0.0, 1.0);
    fixed2 b = floor(n), f = smoothstep(fixed2(0.0,0.0), fixed2(1.0,1.0), frac(n));
    return lerp(lerp(rand(b), rand(b + d.yx), f.x), lerp(rand(b + d.xy), rand(b + d.yy), f.x), f.y);
}

fixed fbm(fixed2 n) {
    fixed total = 0.0, amplitude = 1.0;
    for (int i = 0; i <5; i++) {
        total += noise(n) * amplitude;
        n += n*1.7;
        amplitude *= 0.47;
    }
    return total;
}

fixed4 frag(v2f i) : SV_Target{

    const fixed3 c1 = fixed3(0.5, 0.0, 0.1);
    const fixed3 c2 = fixed3(0.9, 0.1, 0.0);
    const fixed3 c3 = fixed3(0.2, 0.1, 0.7);
    const fixed3 c4 = fixed3(1.0, 0.9, 0.1);
    const fixed3 c5 = fixed3(0.1,0.1,0.1);
    const fixed3 c6 = fixed3(0.9,0.9,0.9);

    fixed2 speed = fixed2(0.1, 0.9);
    fixed shift = 1.327+sin(_Time.y*2.0)/2.4;
    fixed alpha = 1.0;

    fixed dist = 3.5-sin(_Time.y*0.4)/1.89;

    fixed2 uv = i.uv;
    fixed2 p = uv.xy * dist / 1;
    p += sin(p.yx*4.0+fixed2(.2,-.3)*_Time.y)*0.04;
    p += sin(p.yx*8.0+fixed2(.6,+.1)*_Time.y)*0.01;

    p.x -= _Time.y/1.1;
    fixed q = fbm(p - _Time.y * 0.3+1.0*sin(_Time.y+0.5)/2.0);
    fixed qb = fbm(p - _Time.y * 0.4+0.1*cos(_Time.y)/2.0);
    fixed q2 = fbm(p - _Time.y * 0.44 - 5.0*cos(_Time.y)/2.0) - 6.0;
    fixed q3 = fbm(p - _Time.y * 0.9 - 10.0*cos(_Time.y)/15.0)-4.0;
    fixed q4 = fbm(p - _Time.y * 1.4 - 20.0*sin(_Time.y)/14.0)+2.0;
    q = (q + qb - .4 * q2 -2.0*q3  + .6*q4)/3.8;
    fixed2 r = fixed2(fbm(p + q /2.0 + _Time.y * speed.x - p.x - p.y), fbm(p + q - _Time.y * speed.y));
    fixed3 c = lerp(c1, c2, fbm(p + r)) + lerp(c3, c4, r.x) - lerp(c5, c6, r.y);
    fixed3 color = fixed3(1.0/(pow(c+1.61,fixed3(4.0,4.0,4.0))) * cos(shift * uv.y / 1));

    color=fixed3(1.0,.2,.05)/(pow((r.y+r.y)* max(.0,p.y)+0.1, 4.0));;
    color = color/(1.0+max(fixed3(0,0,0),color));
    return  fixed4(color.x, color.y, color.z, alpha);
}ENDCG
}
}
}
\$\endgroup\$
  • 1
    \$\begingroup\$ If you're trying to release a multi-platform game, you really need to work for each platform separately. (Unity usually handles this in the background but this is why it has separate packs for each platform.) Otherwise, you can just make the shader for Android and sacrifice quality on more powerful devices. Just pick one and move forward with that, would be my suggestion. (Not sure if there's a way to do what you need without writing separate shaders for each platform and programmatically setting those shaders to materials in the compilation process) \$\endgroup\$ – John Hamilton Dec 18 '17 at 5:52
  • \$\begingroup\$ @JohnHamilton I'm looking for a way that focuses on Shader, not test .when I write shader first question is is it optimal? I hate this question because Essentially, beauty is important.i following trick to without testing, we're sure it works every where.if I can store pixels from shader this is possible. \$\endgroup\$ – Seyed Morteza Kamali Dec 18 '17 at 6:00
  • \$\begingroup\$ If there's a specific shader & visual effect you're having trouble with, please edit your question to include sample/target visuals, shader code, and a description of what's going wrong. We van help you solve individual problems like that, but there might not be any single piece of advice that will solve every problem you have with shaders at once. ;) \$\endgroup\$ – DMGregory Dec 18 '17 at 6:02
  • \$\begingroup\$ @DMGregory I edited my question(If the description is too much, please fix it) and attached an example.i want using computational or raymarching shaders as VFX in my game with 60 fps!!.i just need trick! \$\endgroup\$ – Seyed Morteza Kamali Dec 18 '17 at 9:18
1
\$\begingroup\$

As user1118321 says in their answer, we can often replace noise or complex functions with texture lookups and still get good results.

Here's an effect I built to try to mimic the first example in the question. It uses two samples into a noise texture, scrolling at different rates, to create chaotic patterns over time.

Animated gif showing a similar glowing ring

(The jump is just due to the short gif loop - the actual effect has a very long seamless looping period)

Texture samples aren't free either - particularly when they're based on a calculated coordinate which makes it harder to hide fetch latency - but if you can exchange heavy math loops for one or two texture samples it can be a net win. Profile it on your target hardware and see.

Shader "Unlit/GlowingRing"
{
    Properties
    {
        // Use an FBM noise texture here.
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                // Transform uvs so (0, 0) is in the center,
                // and we go out to 2.0 at the edges.
                // 1.0 will be the radius of the ring.
                o.uv = (v.uv - 0.5f) * 4.0f;
                return o;
            }       

            fixed4 frag (v2f i) : SV_Target
            {
                // Convert to polar coordinates.
                float2 ringUV = float2(
                                  atan2(i.uv.y, i.uv.x)/(2.0f * 3.141592653589f), 
                                  length(i.uv));

                // Compute our own sampling gradients, and correct
                // the wrap-around from -180 to 180.
                float4 grad = float4(ddx(ringUV), ddy(ringUV));
                grad.xz = frac(grad.xz + 0.5f) - 0.5f;

                // Compute signed distance from the ring at radius 1.
                // > 0 = outside, < 0 = inside.
                float signedDistance = ringUV.y - 1.0f;

                // Our base gradient will fall off away from this ring.
                float level = 1.01f - 1.5f * abs(signedDistance);

                // Flip the texture scrolling direction on the inside.
                float scroll = sign(signedDistance) * _Time.y;

                // Sample the two textures at dfferent scales,
                // moving at two different rates,
                // so they interfere in complex patterns.
                float2 ring1Scale = float2(7.0f, 0.7f);
                float2 ring2Scale = float2(5.0f, 0.7f);

                float2 ring1= ringUV * ring1Scale;
                ring1 -=  scroll * float2(0.11f, 0.31f);
                float2 ring2= ringUV * ring2Scale;
                ring2 -=  scroll * float2(-0.07f, 0.3f);

                float sample1 = tex2Dgrad(_MainTex, ring1, grad.xy * ring1Scale, grad.zw * ring1Scale).r;
                float sample2 = tex2Dgrad(_MainTex, ring2, grad.xy * ring2Scale, grad.zw * ring2Scale).r;

                // Interfere the two texture samples. (I couldn't decide if I liked
                // a sum or ridge noise better, so I used a blend of the two. ;)  )
                float delta = lerp(sample1 + sample2, 1.0f - abs(sample1 - sample2), 0.6f);

                // Squaring this offset enhances contrast between peaks & valleys.
                delta *= delta;

                // Offset our gradient by the texture interference value.
                level += lerp(0.5f, 0.05f, level) * delta;

                // Again multiplying to boost contrast, without flipping negatives.
                level = max(level, 0) * level;
                float level2 = level * level;
                return fixed4(level2 * level2 * level2, 0.85f * level2, level, 1);
            }
            ENDCG
        }
    }
}

It's also worth noting that ShaderToy isn't really meant to be an Asset Store gallery of shaders that you can just drop into your game. A lot of the work there has more in common with tech demos / demoscene creations, meant to push the limits of what we can do in WebGL when rendering one effect alone. It's great to browse for inspirations and to discover new techniques, but for incorporating effects into your game it's generally not safe to just cut & paste and expect it to work correctly/with good performance on all platforms. Instead, take the time to understand the important parts of the effect, and craft your own version that plays nicely with the rest of the rendering going on in your game. ;)

\$\endgroup\$
1
\$\begingroup\$

One thing that immediately stands out to me is the numerous expensive math functions that your shaders are using. Functions like pow, sin, sqrt, etc. are computationally expensive. You should avoid them when you can, and I see some opportunities to do so. For example, this line:

pow(fixed3(0.5, 0.0, .1) + 2.61, fixed3(2.0,2.0,2.0)

You're just squaring a value. You shouldn't call pow to do that. Just multiply it by itself. Multiplication is a single instruction whereas pow is many. You could do this:

fixed3 temp = fixed3(0.5, 0.0, 0.1) + 2.61;
result = temp * temp;

Likewise, this line:

return pow((r.y + r.y) * max(.0, uv.y) + .1, 4.0);

can be written as:

fixed temp = (r.y + r.y) * max(0.0, uv.y) + 0.1;
fixed tempSquared = temp * temp;
return tempSquared * tempSquared;

Functions like sin are periodic. You might be able to get away with a look-up table (texture). (Just be careful that indirect texture look-ups are also slow, so it might not be a win.)

Another thing that might help is passing in a texture of random noise rather than trying to calculate it on the GPU. You can use a smaller texture than the whole image, but index into it cleverly. For example, I've done this where there are a few tiles of random noise. The tiles are uploaded in a different order each frame and the orientation is switched by playing with the texture coordinates. This pushes most of the work to the build system so it can be faster at run time.

Your ramp function uses a conditional. On some architectures (particularly on mobile), conditionals are really slow. You could calculate both sections and use a step function with a lerp to choose between them.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.