games.rednblack.editor.renderer.box2dLight.RayHandler Maven / Gradle / Ivy
Show all versions of runtime-libgdx Show documentation
package games.rednblack.editor.renderer.box2dLight;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.viewport.Viewport;
import games.rednblack.editor.renderer.box2dLight.shaders.LightShader;
import games.rednblack.editor.renderer.box2dLight.shaders.LightWithNormalMapShader;
import static games.rednblack.editor.renderer.systems.render.FrameBufferManager.GL_MAX_TEXTURE_SIZE;
/**
* Handler that manages everything related to lights updating and rendering
* Implements {@link Disposable}
* @author kalle_h
*/
public class RayHandler implements Disposable {
/** Gamma correction value used if enabled
* TODO: remove final modifier and provide method to change
* this default value if needed to anyone? */
static final float GAMMA_COR = 0.625f;
static boolean gammaCorrection = false;
static float gammaCorrectionParameter = 1f;
/**
* TODO: This could be made adaptive to ratio of camera sizes * zoom vs the
* CircleShape radius - thus will provide smooth radial shadows while
* resizing and zooming in and out
*/
static int CIRCLE_APPROX_POINTS = 32;
static float dynamicShadowColorReduction = 1;
static int MAX_SHADOW_VERTICES = 128;
static boolean isDiffuse = false;
/**
* Blend function for lights rendering with both shadows and diffusion
*
Default: (GL20.GL_DST_COLOR, GL20.GL_ZERO)
*/
public final BlendFunc diffuseBlendFunc =
new BlendFunc(GL20.GL_DST_COLOR, GL20.GL_ZERO);
/**
* Blend function for lights rendering with shadows but without diffusion
*
Default: (GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA)
*/
public final BlendFunc shadowBlendFunc =
new BlendFunc(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
/**
* Blend function for lights rendering without shadows and diffusion
*
Default: (GL20.GL_SRC_ALPHA, GL20.GL_ONE)
*/
public final BlendFunc simpleBlendFunc =
new BlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE);
final Matrix4 combined = new Matrix4();
final Color ambientLight = new Color();
/**
* This Array contain all the lights.
*
*
NOTE: DO NOT MODIFY THIS LIST
*/
final Array lightList = new Array(false, 16);
/**
* This Array contain all the disabled lights.
*
* NOTE: DO NOT MODIFY THIS LIST
*/
final Array disabledLights = new Array(false, 16);
LightMap lightMap;
ShaderProgram lightShader;
ShaderProgram lightShaderWithoutNormals;
ShaderProgram lightShaderWithNormals;
ShaderProgram customLightShader = null;
boolean culling = true;
boolean shadows = true;
boolean blur = true;
boolean normalMaps = false;
/** Experimental mode */
boolean pseudo3d = false;
boolean shadowColorInterpolation = false;
int blurNum = 1;
boolean customViewport = false;
int viewportX = 0;
int viewportY = 0;
int viewportWidth = Gdx.graphics.getWidth();
int viewportHeight = Gdx.graphics.getHeight();
private Viewport viewport;
Vector2 tmp = new Vector2();
private Texture normalMapTexture = null;
/** How many lights passed culling and rendered to scene last time */
int lightRenderedLastFrame = 0;
/** camera matrix corners */
float x1, x2, y1, y2;
OrthographicCamera camera;
World world;
/**
* Class constructor specifying the physics world from where collision
* geometry is taken.
*
* NOTE: FBO size is 1/4 * screen size and used by default.
*
* Default setting are:
*
* - culling = true
*
- shadows = true
*
- diffuse = false
*
- blur = true
*
- blurNum = 1
*
- ambientLight = 0f
*
*
* @see #RayHandler(World, int, int, RayHandlerOptions)
*/
public RayHandler(World world) {
this(world, Gdx.graphics.getWidth() / 4, Gdx.graphics
.getHeight() / 4, null);
}
public RayHandler(World world, RayHandlerOptions options) {
this(world, Gdx.graphics.getWidth() / 4, Gdx.graphics
.getHeight() / 4, options);
}
/**
* Class constructor specifying the physics world from where collision
* geometry is taken, and size of FBO used for intermediate rendering.
*
* @see #RayHandler(World)
*/
public RayHandler(World world, int fboWidth, int fboHeight) {
this(world, fboWidth, fboHeight, null);
}
public RayHandler(World world, int fboWidth, int fboHeight, RayHandlerOptions options) {
this.world = world;
if (options != null) {
isDiffuse = options.isDiffuse;
gammaCorrection = options.gammaCorrection;
pseudo3d = options.pseudo3d;
shadowColorInterpolation = options.shadowColorInterpolation;
}
resizeFBO(fboWidth, fboHeight);
}
/**
* Resize the FBO used for intermediate rendering.
*/
public void resizeFBO(int fboWidth, int fboHeight) {
// Check if either width or height exceeds max_size
if (fboWidth > GL_MAX_TEXTURE_SIZE || fboHeight > GL_MAX_TEXTURE_SIZE) {
// Calculate the aspect ratio
double aspectRatio = (double) fboWidth / fboHeight;
// Adjust dimensions while maintaining the aspect ratio
if (fboWidth > fboHeight) {
fboWidth = GL_MAX_TEXTURE_SIZE;
fboHeight = (int) (GL_MAX_TEXTURE_SIZE / aspectRatio);
} else {
fboHeight = GL_MAX_TEXTURE_SIZE;
fboWidth = (int) (GL_MAX_TEXTURE_SIZE * aspectRatio);
}
}
if (lightMap != null) {
lightMap.dispose();
}
lightMap = new LightMap(this, fboWidth, fboHeight);
lightShaderWithoutNormals = LightShader.createLightShader();
lightShaderWithNormals = LightWithNormalMapShader.createLightShader();
lightShader = lightShaderWithoutNormals;
}
/**
* Sets combined matrix basing on camera position, rotation and zoom
*
* Same as calling:
* {@code setCombinedMatrix(
* camera.combined,
* camera.position.x,
* camera.position.y,
* camera.viewportWidth * camera.zoom,
* camera.viewportHeight * camera.zoom );}
*
* @see #setCombinedMatrix(Matrix4, float, float, float, float)
*/
public void setCombinedMatrix(OrthographicCamera camera) {
this.camera = camera;
this.setCombinedMatrix(
camera.combined,
camera.position.x,
camera.position.y,
camera.viewportWidth * camera.zoom,
camera.viewportHeight * camera.zoom);
}
/**
* Sets combined camera matrix.
*
*
Matrix must be set to work in box2d coordinates, it will be copied
* and used for culling and rendering. Remember to update it if camera
* changes. This will work with rotated cameras.
*
* @param combined
* matrix that include projection and translation matrices
* @param x
* combined matrix position
* @param y
* combined matrix position
* @param viewPortWidth
* NOTE!! use actual size, remember to multiple with zoom value
* if pulled from OrthoCamera
* @param viewPortHeight
* NOTE!! use actual size, remember to multiple with zoom value
* if pulled from OrthoCamera
*
* @see #setCombinedMatrix(OrthographicCamera)
*/
public void setCombinedMatrix(Matrix4 combined, float x, float y,
float viewPortWidth, float viewPortHeight) {
System.arraycopy(combined.val, 0, this.combined.val, 0, 16);
// updateCameraCorners
final float halfViewPortWidth = viewPortWidth * 0.5f;
x1 = x - halfViewPortWidth;
x2 = x + halfViewPortWidth;
final float halfViewPortHeight = viewPortHeight * 0.5f;
y1 = y - halfViewPortHeight;
y2 = y + halfViewPortHeight;
}
/**
* Utility method to check if light is on the screen
* @param x - light center x-coord
* @param y - light center y-coord
* @param radius - maximal light distance
*
* @return true if camera screen intersects or contains provided
* light, represented by circle/box area
*/
boolean intersect(float x, float y, float radius) {
return (x1 < (x + radius) && x2 > (x - radius) &&
y1 < (y + radius) && y2 > (y - radius));
}
/**
* Updates and renders all active lights.
*
*
NOTE! Remember to set combined matrix before this method.
*
*
Don't call this inside of any begin/end statements.
* Call this method after you have rendered background but before UI.
* Box2d bodies can be rendered before or after depending how you want
* the x-ray lights to interact with them.
*
* @see #update()
* @see #render()
*/
public void updateAndRender() {
update();
render();
}
/**
* Manual update method for all active lights.
*
*
Use this if you have less physics steps than rendering steps.
*
* @see #updateAndRender()
* @see #render()
*/
public void update() {
for (Light light : lightList) {
light.update();
}
}
/**
* Set a normal map texture with the same size of lights FBO
* to be passed to {@link LightWithNormalMapShader}
*
* @param normalMap texture
*/
public void setNormalMap(Texture normalMap) {
this.normalMapTexture = normalMap;
normalMaps = normalMapTexture != null;
lightShader = normalMaps ? lightShaderWithNormals : lightShaderWithoutNormals;
}
/**
* Prepare all lights for rendering.
*
*
You should need to use this method only if you want to render lights
* on a frame buffer object. Use {@link #render()} otherwise.
*
*
NOTE! Don't call this inside of any begin/end statements.
*
* @see #renderOnly()
* @see #render()
*/
public void prepareRender() {
lightRenderedLastFrame = 0;
Gdx.gl.glDepthMask(false);
Gdx.gl.glEnable(GL20.GL_BLEND);
boolean useLightMap = (shadows || blur);
if (useLightMap) {
lightMap.frameBuffer.begin();
Gdx.gl.glClearColor(0f, 0f, 0f, 0f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
}
simpleBlendFunc.apply();
ShaderProgram shader = customLightShader != null ? customLightShader : lightShader;
shader.bind();
{
if (normalMaps) {
normalMapTexture.bind(0);
lightShader.setUniformi("u_normals", 0);
lightShader.setUniformf("u_resolution", viewportWidth, viewportHeight);
}
lightShader.setUniformMatrix("u_projTrans", combined);
shader.setUniformMatrix("u_projTrans", combined);
if (customLightShader != null) updateLightShader();
for (Light light : lightList) {
if (customLightShader != null) updateLightShaderPerLight(light);
if (normalMaps) {
tmp.set(light.getX(), light.getY());
viewport.project(tmp);
// light position must be normalized
float x = (tmp.x) / viewportWidth;
float y = (tmp.y) / viewportHeight;
lightShader.setUniformf("u_lightpos", x, y, light.pseudo3dHeight * 0.01f);
}
lightShader.setUniformf("u_falloff", light.falloff);
lightShader.setUniformf("u_intensity", light.intensity);
light.render();
}
}
if (useLightMap) {
if (customViewport) {
lightMap.frameBuffer.end(
viewportX,
viewportY,
viewportWidth,
viewportHeight);
} else {
lightMap.frameBuffer.end();
}
}
if (useLightMap && pseudo3d) {
lightMap.shadowBuffer.begin();
Gdx.gl.glClearColor(0f, 0f, 0f, 0f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
for (Light light : lightList) {
light.dynamicShadowRender();
}
if (customViewport) {
lightMap.shadowBuffer.end(
viewportX,
viewportY,
viewportWidth,
viewportHeight);
} else {
lightMap.shadowBuffer.end();
}
}
boolean needed = lightRenderedLastFrame > 0;
// this way lot less binding
if (needed && blur)
lightMap.gaussianBlur(lightMap.frameBuffer, blurNum);
if (needed && blur && pseudo3d)
lightMap.gaussianBlur(lightMap.shadowBuffer, blurNum);
}
/**
* Manual rendering method for all lights.
*
*
NOTE! Remember to set combined matrix and update lights
* before using this method manually.
*
*
Don't call this inside of any begin/end statements.
* Call this method after you have rendered background but before UI.
* Box2d bodies can be rendered before or after depending how you want
* the x-ray lights to interact with them.
*
* @see #updateAndRender()
* @see #update()
* @see #setCombinedMatrix(Matrix4, float, float, float, float)
*/
public void render() {
prepareRender();
lightMap.render();
}
/**
* Manual rendering method for all lights tha can be used inside of
* begin/end statements
*
*
Use this method if you want to render lights in a frame buffer
* object. You must call {@link #prepareRender()} before calling this
* method. Also, {@link #prepareRender()} must not be inside of any
* begin/end statements
*
* @see #prepareRender()
*/
public void renderOnly() {
lightMap.render();
}
/**
* Called before light rendering start
*
* Override this if you are using custom light shader
*/
protected void updateLightShader () {
}
/**
* Called for custom light shader before each light is rendered
*
* Override this if you are using custom light shader
*/
protected void updateLightShaderPerLight (Light light) {
}
/**
* Checks whether the given point is inside of any light volume
*
* @return true if point is inside of any light volume
*/
public boolean pointAtLight(float x, float y) {
for (Light light : lightList) {
if (light.contains(x, y)) return true;
}
return false;
}
/**
* Checks whether the given point is outside of all light volumes
*
* @return true if point is NOT inside of any light volume
*/
public boolean pointAtShadow(float x, float y) {
for (Light light : lightList) {
if (light.contains(x, y)) return false;
}
return true;
}
/**
* Disposes all this rayHandler lights and resources
*/
public void dispose() {
removeAll();
if (lightMap != null) lightMap.dispose();
if (lightShader != null) lightShader.dispose();
}
/**
* Removes and disposes both all active and disabled lights
*/
public void removeAll() {
for (Light light : lightList) {
light.dispose();
}
lightList.clear();
for (Light light : disabledLights) {
light.dispose();
}
disabledLights.clear();
}
/**
* Set custom light shader, null to reset to default
*
* Changes will take effect next time #render() is called
*/
public void setLightShader (ShaderProgram customLightShader) {
this.customLightShader = customLightShader;
}
/**
* Enables/disables culling.
*
*
This save CPU and GPU time when the world is bigger than the screen.
*
*
Default = true
*/
public void setCulling(boolean culling) {
this.culling = culling;
}
/**
* Enables/disables Gaussian blur.
*
*
This make lights much more softer and realistic look but cost some
* precious shader time. With default FBO size on android cost around 1ms.
*
*
Default = true
*
* @see #setBlurNum(int)
*/
public void setBlur(boolean blur) {
this.blur = blur;
}
/**
* Sets number of Gaussian blur passes.
*
*
Blurring can be pretty heavy weight operation, 1-3 should be safe.
* Setting this to 0 is the same as disabling it.
*
*
Default = 1
*
* @see #setBlur(boolean)
*/
public void setBlurNum(int blurNum) {
this.blurNum = blurNum;
}
/**
* Enables/disables shadows
*/
public void setShadows(boolean shadows) {
this.shadows = shadows;
}
/**
* Sets ambient light brightness. Specifies shadows brightness.
*
Default = 0
*
* @param ambientLight
* shadows brightness value, clamped to [0f; 1f]
*
* @see #setAmbientLight(Color)
* @see #setAmbientLight(float, float, float, float)
*/
public void setAmbientLight(float ambientLight) {
this.ambientLight.a = MathUtils.clamp(ambientLight, 0f, 1f);
}
/**
* Sets ambient light color.
* Specifies how shadows colored and their brightness.
*
*
Default = Color(0, 0, 0, 0)
*
* @param r
* shadows color red component
* @param g
* shadows color green component
* @param b
* shadows color blue component
* @param a
* shadows brightness component
*
* @see #setAmbientLight(float)
* @see #setAmbientLight(Color)
*/
public void setAmbientLight(float r, float g, float b, float a) {
this.ambientLight.set(r, g, b, a);
}
/**
* Sets ambient light color.
* Specifies how shadows colored and their brightness.
*
*
Default = Color(0, 0, 0, 0)
*
* @param ambientLightColor
* color whose RGB components specify the shadows coloring and
* alpha specify shadows brightness
*
* @see #setAmbientLight(float)
* @see #setAmbientLight(float, float, float, float)
*/
public void setAmbientLight(Color ambientLightColor) {
this.ambientLight.set(ambientLightColor);
}
/**
* Sets physics world to work with for this rayHandler
*/
public void setWorld(World world) {
this.world = world;
}
/**
* @return if gamma correction is enabled or not
*/
public static boolean getGammaCorrection() {
return gammaCorrection;
}
/**
* Enables/disables gamma correction.
*
*
This need to be done before creating instance of rayHandler.
*
*
NOTE: To match the visuals with gamma uncorrected lights the light
* distance parameters is modified implicitly.
*/
public void applyGammaCorrection(boolean gammaCorrectionWanted) {
gammaCorrection = gammaCorrectionWanted;
gammaCorrectionParameter = gammaCorrection ? GAMMA_COR : 1f;
lightMap.createShaders();
}
/**
* Enables/disables usage of diffuse algorithm.
*
*
If set to true lights are blended using the diffuse shader. This is
* more realistic model than normally used as it preserve colors but might
* look bit darker and also it might improve performance slightly.
*/
public void setDiffuseLight(boolean useDiffuse) {
isDiffuse = useDiffuse;
lightMap.createShaders();
}
public static boolean isDiffuseLight() {
return isDiffuse;
}
public static float getDynamicShadowColorReduction () {
return dynamicShadowColorReduction;
}
/**
* Sets rendering to custom viewport with specified position and size
*
Note: you will be responsible for update of viewport via this method
* in case of any changes (on resize)
*/
public void useCustomViewport(Viewport viewport, int x, int y, int width, int height) {
this.viewport = viewport;
customViewport = true;
viewportX = x;
viewportY = y;
viewportWidth = width;
viewportHeight = height;
}
/**
* Sets rendering to default viewport
*
*
0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()
*/
public void useDefaultViewport() {
customViewport = false;
}
/**
* /!\ Experimental mode with dynamic shadowing in pseudo-3d world
*
* @param flag enable pseudo 3d effect
*/
public void setPseudo3dLight(boolean flag) {
setPseudo3dLight(flag, false);
}
/**
* /!\ Experimental mode with dynamic shadowing in pseudo-3d world
*
* @param flag enable pseudo 3d effect
* @param interpolateShadows interpolate shadow color
*/
public void setPseudo3dLight(boolean flag, boolean interpolateShadows) {
pseudo3d = flag;
shadowColorInterpolation = interpolateShadows;
lightMap.createShaders();
}
/**
* Enables/disables lightMap automatic rendering.
*
*
If set to false user needs to use the {@link #getLightMapTexture()}
* and render that or use it as a light map when rendering. Example shader
* for spriteBatch is given. This is faster way to do if there is not that
* much overdrawing or if just couple object need light/shadows.
*
*
Default = true
*/
public void setLightMapRendering(boolean isAutomatic) {
lightMap.lightMapDrawingDisabled = !isAutomatic;
}
/**
* Expert functionality
*
* @return Texture that contain lightmap texture that can be used as light
* texture in your shaders
*/
public Texture getLightMapTexture() {
return lightMap.frameBuffer.getColorBufferTexture();
}
/**
* Expert functionality, no support given
*
* @return FrameBuffer that contains lightMap
*/
public FrameBuffer getLightMapBuffer() {
return lightMap.frameBuffer;
}
}