Implement animated textures

This commit is contained in:
Lukas Rieger (Blue) 2024-02-26 01:59:28 +01:00
parent 2899646adc
commit aecbd23ba7
No known key found for this signature in database
GPG Key ID: AA33883B1BBA03E6
8 changed files with 265 additions and 23 deletions

View File

@ -294,7 +294,7 @@ export class MapViewer {
}
// render
if (delta >= 1000 || Date.now() - this.lastRedrawChange < 1000) {
if (delta >= 50 || Date.now() - this.lastRedrawChange < 1000) {
this.lastFrame = now;
this.render(delta);
}
@ -325,6 +325,8 @@ export class MapViewer {
if (this.map && this.map.isLoaded) {
this.map.animations.forEach(animation => animation.step(delta))
// shift whole scene including camera towards 0,0 to tackle shader-precision issues
const s = 10000;
const sX = Math.round(this.camera.position.x / s) * s;

View File

@ -39,6 +39,7 @@ import {TileManager} from "./TileManager";
import {TileLoader} from "./TileLoader";
import {LowresTileLoader} from "./LowresTileLoader";
import {reactive} from "vue";
import {TextureAnimation} from "@/js/map/TextureAnimation";
export class Map {
@ -86,6 +87,9 @@ export class Map {
/** @type {Texture[]} */
this.loadedTextures = [];
/** @type {TextureAnimation[]} */
this.animations = [];
/** @type {TileManager} */
this.hiresTileManager = null;
/** @type {TileManager[]} */
@ -264,7 +268,8 @@ export class Map {
* resourcePath: string,
* color: number[],
* halfTransparent: boolean,
* texture: string
* texture: string,
* animation: any | undefined
* }[]} the textures-data
* @returns {ShaderMaterial[]} the hires Material (array because its a multi-material)
*/
@ -293,7 +298,24 @@ export class Map {
texture.wrapT = ClampToEdgeWrapping;
texture.flipY = false;
texture.flatShading = true;
texture.image.addEventListener("load", () => texture.needsUpdate = true);
let animationUniforms = {
animationFrameHeight: { value: 1 },
animationFrameIndex: { value: 0 },
animationInterpolationFrameIndex: { value: 0 },
animationInterpolation: { value: 0 }
};
let animation = null;
if (textureSettings.animation) {
animation = new TextureAnimation(animationUniforms, textureSettings.animation);
this.animations.push(animation);
}
texture.image.addEventListener("load", () => {
texture.needsUpdate = true
if (animation) animation.init(texture.image.naturalWidth, texture.image.naturalHeight)
});
this.loadedTextures.push(texture);
@ -304,7 +326,8 @@ export class Map {
type: 't',
value: texture
},
transparent: { value: transparent }
transparent: { value: transparent },
...animationUniforms
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
@ -363,6 +386,8 @@ export class Map {
this.loadedTextures.forEach(texture => texture.dispose());
this.loadedTextures = [];
this.animations = [];
}
/**

View File

@ -0,0 +1,83 @@
export class TextureAnimation {
/**
* @param uniforms {{
* animationFrameHeight: { value: number },
* animationFrameIndex: { value: number },
* animationInterpolationFrameIndex: { value: number },
* animationInterpolation: { value: number }
* }}
* @param data {{
* interpolate: boolean,
* width: number,
* height: number,
* frametime: number,
* frames: {
* index: number,
* time: number
* }[] | undefined
* }}
*/
constructor(uniforms, data) {
this.uniforms = uniforms;
this.data = {
interpolate: false,
width: 1,
height: 1,
frametime: 1,
...data
};
this.frameImages = 1;
this.frameDelta = 0;
this.frameTime = this.data.frametime * 50;
this.frames = 1;
this.frameIndex = 0;
}
/**
* @param width {number}
* @param height {number}
*/
init(width, height) {
this.frameImages = height / width;
this.uniforms.animationFrameHeight.value = 1 / this.frameImages;
this.frames = this.frameImages;
if (this.data.frames) {
this.frames = this.data.frames.length;
}
}
/**
* @param delta {number}
*/
step(delta) {
this.frameDelta += delta;
if (this.frameDelta > this.frameTime) {
this.frameDelta -= this.frameTime;
this.frameDelta %= this.frameTime;
this.frameIndex++;
this.frameIndex %= this.frames;
if (this.data.frames) {
let frame = this.data.frames[this.frameIndex]
let nextFrame = this.data.frames[(this.frameIndex + 1) % this.frames];
this.uniforms.animationFrameIndex.value = frame.index;
this.uniforms.animationInterpolationFrameIndex.value = nextFrame.index;
this.frameTime = frame.time * 50;
} else {
this.uniforms.animationFrameIndex.value = this.frameIndex;
this.uniforms.animationInterpolationFrameIndex.value = (this.frameIndex + 1) % this.frames;
}
}
if (this.data.interpolate) {
this.uniforms.animationInterpolation.value = this.frameDelta / this.frameTime;
}
}
}

View File

@ -34,6 +34,10 @@ ${ShaderChunk.logdepthbuf_pars_fragment}
uniform sampler2D textureImage;
uniform float sunlightStrength;
uniform float ambientLight;
uniform float animationFrameHeight;
uniform float animationFrameIndex;
uniform float animationInterpolationFrameIndex;
uniform float animationInterpolation;
varying vec3 vPosition;
//varying vec3 vWorldPosition;
@ -46,7 +50,12 @@ varying float vBlocklight;
//varying float vDistance;
void main() {
vec4 color = texture(textureImage, vUv);
vec4 color = texture(textureImage, vec2(vUv.x, animationFrameHeight * (vUv.y + animationFrameIndex)));
if (animationInterpolation > 0.0) {
color = mix(color, texture(textureImage, vec2(vUv.x, animationFrameHeight * (vUv.y + animationInterpolationFrameIndex))), animationInterpolation);
}
if (color.a <= 0.01) discard;
//apply vertex-color

View File

@ -33,7 +33,7 @@ export const VEC3_Z = new Vector3(0, 0, 1);
/**
* Converts a url-encoded image string to an actual image-element
* @param string {string}
* @returns {HTMLElement}
* @returns {HTMLImageElement}
*/
export const stringToImage = string => {
let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');

View File

@ -39,6 +39,7 @@ import de.bluecolored.bluemap.core.resources.biome.BiomeConfig;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.TextureVariable;
import de.bluecolored.bluemap.core.resources.resourcepack.blockstate.BlockState;
import de.bluecolored.bluemap.core.resources.resourcepack.texture.AnimationMeta;
import de.bluecolored.bluemap.core.resources.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.util.Tristate;
import de.bluecolored.bluemap.core.world.Biome;
@ -50,6 +51,8 @@ import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
@ -381,9 +384,23 @@ public class ResourcePack {
ResourcePath<Texture> resourcePath = new ResourcePath<>(root.relativize(file));
if (!usedTextures.contains(resourcePath)) return null; // don't load unused textures
// load image
BufferedImage image;
try (InputStream in = Files.newInputStream(file)) {
return Texture.from(resourcePath, ImageIO.read(in), Files.exists(file.resolveSibling(file.getFileName() + ".mcmeta")));
image = ImageIO.read(in);
}
// load animation
AnimationMeta animation = null;
Path animationPathFile = file.resolveSibling(file.getFileName() + ".mcmeta");
if (Files.exists(animationPathFile)) {
try (Reader in = Files.newBufferedReader(animationPathFile, StandardCharsets.UTF_8)) {
animation = ResourcesGson.INSTANCE.fromJson(in, AnimationMeta.class);
}
}
return Texture.from(resourcePath, image, animation);
}, textures));
} catch (RuntimeException ex) {

View File

@ -0,0 +1,113 @@
package de.bluecolored.bluemap.core.resources.resourcepack.texture;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import de.bluecolored.bluemap.core.resources.AbstractTypeAdapterFactory;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Getter
@JsonAdapter(AnimationMeta.Adapter.class)
public class AnimationMeta {
private boolean interpolate = false;
private int width = 1;
private int height = 1;
private int frametime = 1;
@Nullable private List<FrameMeta> frames = null;
@Getter
@AllArgsConstructor
public static class FrameMeta {
private int index;
private int time;
}
static class Adapter extends AbstractTypeAdapterFactory<AnimationMeta> {
public Adapter() {
super(AnimationMeta.class);
}
@Override
public AnimationMeta read(JsonReader in, Gson gson) throws IOException {
AnimationMeta animationMeta = new AnimationMeta();
in.beginObject();
while (in.hasNext()) {
if (!in.nextName().equals("animation")){
in.skipValue();
continue;
}
in.beginObject();
while (in.hasNext()) {
switch (in.nextName()) {
case "interpolate" : animationMeta.interpolate = in.nextBoolean(); break;
case "width" : animationMeta.width = in.nextInt(); break;
case "height" : animationMeta.height = in.nextInt(); break;
case "frametime" : animationMeta.frametime = in.nextInt(); break;
case "frames" : readFramesList(in, animationMeta); break;
default: in.skipValue(); break;
}
}
in.endObject();
}
in.endObject();
// default frame-time
if (animationMeta.frames != null) {
for (FrameMeta frameMeta : animationMeta.frames) {
if (frameMeta.time == -1) frameMeta.time = animationMeta.frametime;
}
}
return animationMeta;
}
private void readFramesList(JsonReader in, AnimationMeta animationMeta) throws IOException {
animationMeta.frames = new ArrayList<>();
in.beginArray();
while (in.hasNext()) {
int index = 0;
int time = -1;
if (in.peek() == JsonToken.NUMBER) {
index = in.nextInt();
} else {
in.beginObject();
while (in.hasNext()) {
switch (in.nextName()) {
case "index" : index = in.nextInt(); break;
case "time" : time = in.nextInt(); break;
default: in.skipValue(); break;
}
}
in.endObject();
}
animationMeta.frames.add(new FrameMeta(index, time));
}
in.endArray();
}
@Override
public void write(JsonWriter out, AnimationMeta value, Gson gson) throws IOException {
gson.getDelegateAdapter(this, TypeToken.get(AnimationMeta.class)).write(out, value);
}
}
}

View File

@ -28,6 +28,7 @@ import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.util.BufferedImageUtil;
import de.bluecolored.bluemap.core.util.math.Color;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
@ -42,24 +43,27 @@ public class Texture {
new ResourcePath<>("bluemap", "missing"),
new Color().set(0.5f, 0f, 0.5f, 1.0f, false),
false,
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAPklEQVR4Xu3MsQkAMAwDQe2/tFPnBB4gpLhG8MpkZpNkZ6AKZKAKZKAKZKAKZKAKZKAKZKAKWg0XD/UPnjg4MbX+EDdeTUwAAAAASUVORK5CYII\u003d"
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAPklEQVR4Xu3MsQkAMAwDQe2/tFPnBB4gpLhG8MpkZpNkZ6AKZKAKZKAKZKAKZKAKZKAKZKAKWg0XD/UPnjg4MbX+EDdeTUwAAAAASUVORK5CYII\u003d",
null
);
private ResourcePath<Texture> resourcePath;
private Color color;
private boolean halfTransparent;
private String texture;
@Nullable private AnimationMeta animation;
private transient Color colorPremultiplied;
@SuppressWarnings("unused")
private Texture() {}
private Texture(ResourcePath<Texture> resourcePath, Color color, boolean halfTransparent, String texture) {
private Texture(ResourcePath<Texture> resourcePath, Color color, boolean halfTransparent, String texture, @Nullable AnimationMeta animation) {
this.resourcePath = resourcePath;
this.color = color.straight();
this.halfTransparent = halfTransparent;
this.texture = texture;
this.animation = animation;
}
private Texture(ResourcePath<Texture> resourcePath) {
@ -67,6 +71,7 @@ public class Texture {
this.color = MISSING.color;
this.halfTransparent = MISSING.halfTransparent;
this.texture = MISSING.texture;
this.animation = null;
}
public ResourcePath<Texture> getResourcePath() {
@ -95,19 +100,7 @@ public class Texture {
return texture;
}
public void unloadImageData() {
texture = null;
}
public static Texture from(ResourcePath<Texture> resourcePath, BufferedImage image) throws IOException {
return from(resourcePath, image, true);
}
public static Texture from(ResourcePath<Texture> resourcePath, BufferedImage image, boolean animated) throws IOException {
//crop off animation frames
if (animated && image.getHeight() > image.getWidth()){
image = image.getSubimage(0, 0, image.getWidth(), image.getWidth());
}
public static Texture from(ResourcePath<Texture> resourcePath, BufferedImage image, @Nullable AnimationMeta animation) throws IOException {
//check halfTransparency
boolean halfTransparent = checkHalfTransparent(image);
@ -120,7 +113,7 @@ public class Texture {
ImageIO.write(image, "png", os);
String base64 = "data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray());
return new Texture(resourcePath, color, halfTransparent, base64);
return new Texture(resourcePath, color, halfTransparent, base64, animation);
}
private static boolean checkHalfTransparent(BufferedImage image){