简体   繁体   中英

Making a sprite repeat when resized (libGDX)

I'm making a game with a background texture which is stored as a sprite because I need to resize it. However, when I resize it, it changes the aspect ratio of the image instead of repeating it. The texture I passed in to create the Sprite has the wrap set to Texture.TextureWrap.Repeat.

My current class:

package com.lance.seajam;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;

public class Water extends ApplicationAdapter {
    private final Texture waterTexture;
    private final Texture noiseTexture;
    private SpriteBatch batch;
    private Sprite sprite;
    private ShaderProgram shaderProgram;

    private String vertexShaderString = Gdx.files.internal("shaders/water/mainvs.glsl").readString();
    private String fragmentShaderString = Gdx.files.internal("shaders/water/mainfs.glsl").readString();

    private float[] floatArrOf(float... A) {
        return A;
    }


    private void compileShader() {
        shaderProgram = new ShaderProgram(vertexShaderString, fragmentShaderString);
        if (!shaderProgram.isCompiled()) {
            System.out.println(shaderProgram.getLog());
        }
    }

    public Water(SpriteBatch batch, String imgDir) {
        this.batch = batch;
        waterTexture = new Texture(Gdx.files.internal(imgDir + "/water.png"));
        noiseTexture = new Texture(Gdx.files.internal(imgDir + "/noise.png"));

        noiseTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat); // make the texture repeat
        waterTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat); // make the texture repeat

        waterTexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
        noiseTexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);

        sprite = new Sprite(waterTexture);
        sprite.setSize((float) Gdx.graphics.getWidth(), (float) Gdx.graphics.getHeight()); // setting sprite size to graphics size

        compileShader();
    }

    float time = 0f;
    public void Draw() {
        shaderProgram.setUniformf("u_noise_scale", 0.1f);
        shaderProgram.setUniform2fv("u_noise_scroll_velocity", floatArrOf(0.004f, 0.003f), 0, 2);
        shaderProgram.setUniformf("u_distortion", 0.04f);
        shaderProgram.setUniformf("u_time", time);

        noiseTexture.bind();

        batch.begin();
            time += Gdx.graphics.getDeltaTime(); // Gets how much seconds has passed

            Gdx.gl.glEnable(GL20.GL_BLEND);

            batch.setShader(shaderProgram);
            batch.draw(sprite, sprite.getX(), sprite.getY(), sprite.getWidth(), sprite.getHeight());

            Gdx.gl.glDisable(GL20.GL_BLEND);
        batch.end();
    }
}

Is there any way that I can change it to repeat when resizing? If so, how?

Never use batch.draw(sprite, ...) . Sprites should always be drawn using sprite.draw(batch) . There was an unfortunate design decision in the early days of libGDX to make Sprite a subclass of TextureRegion, which makes it possible to call batch.draw(sprite, ...) , but this should never be done. Sprite already defines its size, scale, color, etc. When you treat it like a TextureRegion by calling batch.draw() , all that information is ignored unless you manually also pass those associated properties of the Sprite.

In my opinion, the Sprite class should almost never be used at all, because it conflates asset data (a TextureRegion) with model data (position, size, scale, rotation, etc.). It is only appropriate for situations where these attributes should be highly optimized, like in a particle system (which is how libGDX does it's own 2D particle system).

Currently you are in the danger zone of using a shared SpriteBatch without any specific projection matrix, and are not doing anything in resize() . This means this class completely ignores screen dimensions and it will look completely different on different devices. Even two phones with the same size screen could look completely different if they have different DPI.

For this particular shader, where it just fills the screen with an effect, I think it's appropriate to use an identity matrix and let the shader size the effect. You can draw your texture to cover the +/- 1 X and Y square in identity space and it will fill the screen regardless of screen dimensions.

To make the texture repeat in a shader like this, you can either use U2 and V2 texture coordinates that are bigger than 1 when passing it to draw , or you can pass a uniform u_textureRepeat 2-element array for the vertex shader to multiply by the texture coordinates. The second way is probably easier to get right. It also allows you to use a different value for the second noise texture if it needs to be scaled a different number of times.

You'll want some constant value for the number of repeats across or down so you can scale by screen ratio. You probably also want your noise scale to be handled the same way, or it will be stretched differently on different aspect ratio screens.

You need to bind your shader before setting its uniforms. It might be working now, but when other shaders are in your app, it will cause problems.

You need to specifically bind your noise texture to unit 1. You are currently binding it to an arbitrary unit (whatever the last thing that interacted with glActiveTexture did), and you shouldn't leave that to chance.

Don't use glEnable and glDisable for blending when using SpriteBatch. SpriteBatch already internally handles its blending.

Pulling it all together looks something like this (I'm doing this from memory without testing so please excuse syntax errors):

public class Water extends ApplicationAdapter implements Disposable {
    private static final float TEXTURE_REPEATS_HORIZONTAL = 5f;
    private static final float NOISE_SPEED_HORIZONTAL = 0.004f;
    private static final Matrix4 IDT = new Matrix4();

    private final Texture waterTexture;
    private final Texture noiseTexture;
    private final SpriteBatch batch;
    private final ShaderProgram shaderProgram;

    private final String vertexShaderString = Gdx.files.internal("shaders/water/mainvs.glsl").readString();
    private final String fragmentShaderString = Gdx.files.internal("shaders/water/mainfs.glsl").readString();

    private void compileShader() {
        shaderProgram = new ShaderProgram(vertexShaderString, fragmentShaderString);
        if (!shaderProgram.isCompiled()) {
            System.out.println(shaderProgram.getLog());
        }
    }

    public Water(SpriteBatch batch, String imgDir) {
        this.batch = batch;
        waterTexture = new Texture(Gdx.files.internal(imgDir + "/water.png"));
        noiseTexture = new Texture(Gdx.files.internal(imgDir + "/noise.png"));

        noiseTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat); // make the texture repeat
        waterTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat); // make the texture repeat

        waterTexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
        noiseTexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);

        compileShader();
    }

    @Override
    public void dispose() {
        waterTexture.dispose();
        noiseTexture.dispose();
        shaderProgram.dispose();
    }

    float time = 0f;
    public void Draw() {
        time += Gdx.graphics.getDeltaTime(); // Gets how much seconds has passed

        shaderProgram.bind();

        float screenRatio = ((float)Gdx.graphics.getWidth()) / Gdx.graphics.getHeight();
        shaderProgram.setUniform2f("u_texCoordScale", TEXTURE_REPEATS_HORIZONTAL, TEXTURE_REPEATS_HORIZONTAL / screenRatio);
        shaderProgram.setUniformf("u_noise_scale", 0.1f);
        shaderProgram.setUniform2f("u_noise_scroll_velocity", NOISE_SPEED_HORIZONTAL, NOISE_SPEED_HORIZONTAL / screenRatio);
        shaderProgram.setUniformf("u_distortion", 0.04f);
        shaderProgram.setUniformf("u_time", time);

        noiseTexture.bind(1);

        batch.begin();
            batch.setProjectionMatrix(IDT);
            batch.enableBlending();
            batch.setShader(shaderProgram);
            batch.draw(waterTexture, -1f, 1f, 2f, -2f); // screen-covering square for identity matrix
        batch.end();
        batch.setShader(null); // reset to default for other classes that use same batch
    }
}

Notice I implement Disposable. Since this class instantiates some Disposable objects, it must be responsible also for disposing them. And whichever class instantiates this one must be responsible for disposing of it. Otherwise you will have memory leaks.

Also note it's usually easier to use shaderProgram.setUniformf than setUniformfv . You only need the latter when using a single array for various unrelated parameters.

Somewhere in your vertex shader, you should have something like v_texCoords = aTexCoord0; You should add the uniform for uniform vec2 u_texCoordScale and on this line multiply it so it is v_texCoords = aTexCoord0 * u_texCoordScale; . If you end up needing a different scale for noise coordinates, you can create associated code in your class for calculating a different uniform for multiplying by the noise's tex coords.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM