Skip to content

Commit aecbd23

Browse files
committed
Implement animated textures
1 parent 2899646 commit aecbd23

File tree

8 files changed

+265
-23
lines changed

8 files changed

+265
-23
lines changed

BlueMapCommon/webapp/src/js/MapViewer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ export class MapViewer {
294294
}
295295

296296
// render
297-
if (delta >= 1000 || Date.now() - this.lastRedrawChange < 1000) {
297+
if (delta >= 50 || Date.now() - this.lastRedrawChange < 1000) {
298298
this.lastFrame = now;
299299
this.render(delta);
300300
}
@@ -325,6 +325,8 @@ export class MapViewer {
325325

326326
if (this.map && this.map.isLoaded) {
327327

328+
this.map.animations.forEach(animation => animation.step(delta))
329+
328330
// shift whole scene including camera towards 0,0 to tackle shader-precision issues
329331
const s = 10000;
330332
const sX = Math.round(this.camera.position.x / s) * s;

BlueMapCommon/webapp/src/js/map/Map.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {TileManager} from "./TileManager";
3939
import {TileLoader} from "./TileLoader";
4040
import {LowresTileLoader} from "./LowresTileLoader";
4141
import {reactive} from "vue";
42+
import {TextureAnimation} from "@/js/map/TextureAnimation";
4243

4344
export class Map {
4445

@@ -86,6 +87,9 @@ export class Map {
8687
/** @type {Texture[]} */
8788
this.loadedTextures = [];
8889

90+
/** @type {TextureAnimation[]} */
91+
this.animations = [];
92+
8993
/** @type {TileManager} */
9094
this.hiresTileManager = null;
9195
/** @type {TileManager[]} */
@@ -264,7 +268,8 @@ export class Map {
264268
* resourcePath: string,
265269
* color: number[],
266270
* halfTransparent: boolean,
267-
* texture: string
271+
* texture: string,
272+
* animation: any | undefined
268273
* }[]} the textures-data
269274
* @returns {ShaderMaterial[]} the hires Material (array because its a multi-material)
270275
*/
@@ -293,7 +298,24 @@ export class Map {
293298
texture.wrapT = ClampToEdgeWrapping;
294299
texture.flipY = false;
295300
texture.flatShading = true;
296-
texture.image.addEventListener("load", () => texture.needsUpdate = true);
301+
302+
let animationUniforms = {
303+
animationFrameHeight: { value: 1 },
304+
animationFrameIndex: { value: 0 },
305+
animationInterpolationFrameIndex: { value: 0 },
306+
animationInterpolation: { value: 0 }
307+
};
308+
309+
let animation = null;
310+
if (textureSettings.animation) {
311+
animation = new TextureAnimation(animationUniforms, textureSettings.animation);
312+
this.animations.push(animation);
313+
}
314+
315+
texture.image.addEventListener("load", () => {
316+
texture.needsUpdate = true
317+
if (animation) animation.init(texture.image.naturalWidth, texture.image.naturalHeight)
318+
});
297319

298320
this.loadedTextures.push(texture);
299321

@@ -304,7 +326,8 @@ export class Map {
304326
type: 't',
305327
value: texture
306328
},
307-
transparent: { value: transparent }
329+
transparent: { value: transparent },
330+
...animationUniforms
308331
},
309332
vertexShader: vertexShader,
310333
fragmentShader: fragmentShader,
@@ -363,6 +386,8 @@ export class Map {
363386

364387
this.loadedTextures.forEach(texture => texture.dispose());
365388
this.loadedTextures = [];
389+
390+
this.animations = [];
366391
}
367392

368393
/**
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
2+
3+
export class TextureAnimation {
4+
5+
/**
6+
* @param uniforms {{
7+
* animationFrameHeight: { value: number },
8+
* animationFrameIndex: { value: number },
9+
* animationInterpolationFrameIndex: { value: number },
10+
* animationInterpolation: { value: number }
11+
* }}
12+
* @param data {{
13+
* interpolate: boolean,
14+
* width: number,
15+
* height: number,
16+
* frametime: number,
17+
* frames: {
18+
* index: number,
19+
* time: number
20+
* }[] | undefined
21+
* }}
22+
*/
23+
constructor(uniforms, data) {
24+
this.uniforms = uniforms;
25+
this.data = {
26+
interpolate: false,
27+
width: 1,
28+
height: 1,
29+
frametime: 1,
30+
...data
31+
};
32+
this.frameImages = 1;
33+
this.frameDelta = 0;
34+
this.frameTime = this.data.frametime * 50;
35+
this.frames = 1;
36+
this.frameIndex = 0;
37+
}
38+
39+
/**
40+
* @param width {number}
41+
* @param height {number}
42+
*/
43+
init(width, height) {
44+
this.frameImages = height / width;
45+
this.uniforms.animationFrameHeight.value = 1 / this.frameImages;
46+
this.frames = this.frameImages;
47+
if (this.data.frames) {
48+
this.frames = this.data.frames.length;
49+
}
50+
}
51+
52+
/**
53+
* @param delta {number}
54+
*/
55+
step(delta) {
56+
this.frameDelta += delta;
57+
58+
if (this.frameDelta > this.frameTime) {
59+
this.frameDelta -= this.frameTime;
60+
this.frameDelta %= this.frameTime;
61+
62+
this.frameIndex++;
63+
this.frameIndex %= this.frames;
64+
65+
if (this.data.frames) {
66+
let frame = this.data.frames[this.frameIndex]
67+
let nextFrame = this.data.frames[(this.frameIndex + 1) % this.frames];
68+
69+
this.uniforms.animationFrameIndex.value = frame.index;
70+
this.uniforms.animationInterpolationFrameIndex.value = nextFrame.index;
71+
this.frameTime = frame.time * 50;
72+
} else {
73+
this.uniforms.animationFrameIndex.value = this.frameIndex;
74+
this.uniforms.animationInterpolationFrameIndex.value = (this.frameIndex + 1) % this.frames;
75+
}
76+
}
77+
78+
if (this.data.interpolate) {
79+
this.uniforms.animationInterpolation.value = this.frameDelta / this.frameTime;
80+
}
81+
}
82+
83+
}

BlueMapCommon/webapp/src/js/map/hires/HiresFragmentShader.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ ${ShaderChunk.logdepthbuf_pars_fragment}
3434
uniform sampler2D textureImage;
3535
uniform float sunlightStrength;
3636
uniform float ambientLight;
37+
uniform float animationFrameHeight;
38+
uniform float animationFrameIndex;
39+
uniform float animationInterpolationFrameIndex;
40+
uniform float animationInterpolation;
3741
3842
varying vec3 vPosition;
3943
//varying vec3 vWorldPosition;
@@ -46,7 +50,12 @@ varying float vBlocklight;
4650
//varying float vDistance;
4751
4852
void main() {
49-
vec4 color = texture(textureImage, vUv);
53+
54+
vec4 color = texture(textureImage, vec2(vUv.x, animationFrameHeight * (vUv.y + animationFrameIndex)));
55+
if (animationInterpolation > 0.0) {
56+
color = mix(color, texture(textureImage, vec2(vUv.x, animationFrameHeight * (vUv.y + animationInterpolationFrameIndex))), animationInterpolation);
57+
}
58+
5059
if (color.a <= 0.01) discard;
5160
5261
//apply vertex-color

BlueMapCommon/webapp/src/js/util/Utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const VEC3_Z = new Vector3(0, 0, 1);
3333
/**
3434
* Converts a url-encoded image string to an actual image-element
3535
* @param string {string}
36-
* @returns {HTMLElement}
36+
* @returns {HTMLImageElement}
3737
*/
3838
export const stringToImage = string => {
3939
let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');

BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resources/resourcepack/ResourcePack.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.BlockModel;
4040
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.TextureVariable;
4141
import de.bluecolored.bluemap.core.resources.resourcepack.blockstate.BlockState;
42+
import de.bluecolored.bluemap.core.resources.resourcepack.texture.AnimationMeta;
4243
import de.bluecolored.bluemap.core.resources.resourcepack.texture.Texture;
4344
import de.bluecolored.bluemap.core.util.Tristate;
4445
import de.bluecolored.bluemap.core.world.Biome;
@@ -50,6 +51,8 @@
5051
import java.io.BufferedReader;
5152
import java.io.IOException;
5253
import java.io.InputStream;
54+
import java.io.Reader;
55+
import java.nio.charset.StandardCharsets;
5356
import java.nio.file.FileSystem;
5457
import java.nio.file.FileSystems;
5558
import java.nio.file.Files;
@@ -381,9 +384,23 @@ private void loadTextures(Path root) throws IOException {
381384
ResourcePath<Texture> resourcePath = new ResourcePath<>(root.relativize(file));
382385
if (!usedTextures.contains(resourcePath)) return null; // don't load unused textures
383386

387+
// load image
388+
BufferedImage image;
384389
try (InputStream in = Files.newInputStream(file)) {
385-
return Texture.from(resourcePath, ImageIO.read(in), Files.exists(file.resolveSibling(file.getFileName() + ".mcmeta")));
390+
image = ImageIO.read(in);
386391
}
392+
393+
// load animation
394+
AnimationMeta animation = null;
395+
Path animationPathFile = file.resolveSibling(file.getFileName() + ".mcmeta");
396+
if (Files.exists(animationPathFile)) {
397+
try (Reader in = Files.newBufferedReader(animationPathFile, StandardCharsets.UTF_8)) {
398+
animation = ResourcesGson.INSTANCE.fromJson(in, AnimationMeta.class);
399+
}
400+
}
401+
402+
return Texture.from(resourcePath, image, animation);
403+
387404
}, textures));
388405

389406
} catch (RuntimeException ex) {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package de.bluecolored.bluemap.core.resources.resourcepack.texture;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.annotations.JsonAdapter;
5+
import com.google.gson.reflect.TypeToken;
6+
import com.google.gson.stream.JsonReader;
7+
import com.google.gson.stream.JsonToken;
8+
import com.google.gson.stream.JsonWriter;
9+
import de.bluecolored.bluemap.core.resources.AbstractTypeAdapterFactory;
10+
import lombok.AllArgsConstructor;
11+
import lombok.Getter;
12+
import org.jetbrains.annotations.Nullable;
13+
14+
import java.io.IOException;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
18+
@Getter
19+
@JsonAdapter(AnimationMeta.Adapter.class)
20+
public class AnimationMeta {
21+
22+
private boolean interpolate = false;
23+
private int width = 1;
24+
private int height = 1;
25+
private int frametime = 1;
26+
27+
@Nullable private List<FrameMeta> frames = null;
28+
29+
@Getter
30+
@AllArgsConstructor
31+
public static class FrameMeta {
32+
private int index;
33+
private int time;
34+
}
35+
36+
static class Adapter extends AbstractTypeAdapterFactory<AnimationMeta> {
37+
38+
public Adapter() {
39+
super(AnimationMeta.class);
40+
}
41+
42+
@Override
43+
public AnimationMeta read(JsonReader in, Gson gson) throws IOException {
44+
AnimationMeta animationMeta = new AnimationMeta();
45+
46+
in.beginObject();
47+
while (in.hasNext()) {
48+
if (!in.nextName().equals("animation")){
49+
in.skipValue();
50+
continue;
51+
}
52+
53+
in.beginObject();
54+
while (in.hasNext()) {
55+
switch (in.nextName()) {
56+
case "interpolate" : animationMeta.interpolate = in.nextBoolean(); break;
57+
case "width" : animationMeta.width = in.nextInt(); break;
58+
case "height" : animationMeta.height = in.nextInt(); break;
59+
case "frametime" : animationMeta.frametime = in.nextInt(); break;
60+
case "frames" : readFramesList(in, animationMeta); break;
61+
default: in.skipValue(); break;
62+
}
63+
}
64+
in.endObject();
65+
66+
}
67+
in.endObject();
68+
69+
// default frame-time
70+
if (animationMeta.frames != null) {
71+
for (FrameMeta frameMeta : animationMeta.frames) {
72+
if (frameMeta.time == -1) frameMeta.time = animationMeta.frametime;
73+
}
74+
}
75+
76+
return animationMeta;
77+
}
78+
79+
private void readFramesList(JsonReader in, AnimationMeta animationMeta) throws IOException {
80+
animationMeta.frames = new ArrayList<>();
81+
82+
in.beginArray();
83+
while (in.hasNext()) {
84+
int index = 0;
85+
int time = -1;
86+
87+
if (in.peek() == JsonToken.NUMBER) {
88+
index = in.nextInt();
89+
} else {
90+
in.beginObject();
91+
while (in.hasNext()) {
92+
switch (in.nextName()) {
93+
case "index" : index = in.nextInt(); break;
94+
case "time" : time = in.nextInt(); break;
95+
default: in.skipValue(); break;
96+
}
97+
}
98+
in.endObject();
99+
}
100+
101+
animationMeta.frames.add(new FrameMeta(index, time));
102+
}
103+
in.endArray();
104+
}
105+
106+
@Override
107+
public void write(JsonWriter out, AnimationMeta value, Gson gson) throws IOException {
108+
gson.getDelegateAdapter(this, TypeToken.get(AnimationMeta.class)).write(out, value);
109+
}
110+
111+
}
112+
113+
}

0 commit comments

Comments
 (0)