mirror of
https://github.com/BlueMap-Minecraft/BlueMap.git
synced 2024-11-21 18:16:09 +01:00
Implement animated textures
This commit is contained in:
parent
2899646adc
commit
aecbd23ba7
@ -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;
|
||||
|
@ -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 = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
83
BlueMapCommon/webapp/src/js/map/TextureAnimation.js
Normal file
83
BlueMapCommon/webapp/src/js/map/TextureAnimation.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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');
|
||||
|
@ -39,6 +39,7 @@
|
||||
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.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 @@ private void loadTextures(Path root) throws IOException {
|
||||
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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -28,6 +28,7 @@
|
||||
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 @@ private Texture(ResourcePath<Texture> resourcePath) {
|
||||
this.color = MISSING.color;
|
||||
this.halfTransparent = MISSING.halfTransparent;
|
||||
this.texture = MISSING.texture;
|
||||
this.animation = null;
|
||||
}
|
||||
|
||||
public ResourcePath<Texture> getResourcePath() {
|
||||
@ -95,19 +100,7 @@ public String getTexture() {
|
||||
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 static Texture from(ResourcePath<Texture> resourcePath, BufferedImage ima
|
||||
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){
|
||||
|
Loading…
Reference in New Issue
Block a user