Intial Commit
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package localmods.mtkviewpoint;
|
||||
|
||||
import me.zed_0xff.zombie_buddy.Exposer.LuaClass;
|
||||
import viewpoint.core.View;
|
||||
import viewpoint.input.FreeCam;
|
||||
import viewpoint.input.Look;
|
||||
|
||||
/** Read-only bridge to Viewpoint 0.1.6.1; never changes capture or camera state. */
|
||||
@LuaClass(name = "MTKViewpointCamera")
|
||||
public final class CameraBridge {
|
||||
private CameraBridge() {}
|
||||
|
||||
public static boolean enabled() {
|
||||
return View.enabled;
|
||||
}
|
||||
|
||||
public static boolean captured() {
|
||||
return View.enabled && Look.captured && !FreeCam.active;
|
||||
}
|
||||
|
||||
public static Double heading() {
|
||||
float yaw = Look.yaw;
|
||||
if (!captured() || !Float.isFinite(yaw)) return null;
|
||||
// Viewpoint Controls.face uses cos(yaw), sin(yaw) in game-world XY.
|
||||
return (Math.toDegrees(yaw) % 360.0 + 360.0) % 360.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package localmods.mtkviewpoint;
|
||||
|
||||
public final class Main {
|
||||
private Main() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("[MTKViewpointTurrets] 0.2.0 camera bridge and 3D tracers loaded (Viewpoint 0.1.6.1 / B42.21).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package localmods.mtkviewpoint;
|
||||
|
||||
import java.util.*;
|
||||
import me.zed_0xff.zombie_buddy.Exposer.LuaClass;
|
||||
import se.krka.kahlua.vm.*;
|
||||
import viewpoint.core.Frame;
|
||||
import viewpoint.render.SceneData;
|
||||
import zombie.Lua.Event;
|
||||
import zombie.Lua.LuaEventManager;
|
||||
|
||||
/** Reads MTK's real projectiles on the game thread; publishes immutable render snapshots. */
|
||||
@LuaClass(name = "MTKViewpointProjectiles")
|
||||
public final class ProjectileBridge {
|
||||
static final int LIMIT = 256;
|
||||
static final double LEVEL = 2.4494895935058594;
|
||||
private static KahluaTable physics, visuals;
|
||||
private static final Map<SceneData, List<TracerMesh.Segment>> frames =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private ProjectileBridge() {}
|
||||
|
||||
public static LuaClosure legacyRenderer() {
|
||||
var events = new ArrayList<Event>();
|
||||
LuaEventManager.getEvents(events, new HashMap<>());
|
||||
LuaClosure found = null;
|
||||
for (var event : events) {
|
||||
if (!"OnPreUIDraw".equals(event.name)) continue;
|
||||
for (var callback : event.callbacks) {
|
||||
var constants = Arrays.asList(callback.prototype.constants);
|
||||
if (constants.containsAll(List.of("renderline","VisualProjectilePC","currentDistance","startHeight","isCovered"))) {
|
||||
if (found != null) return null; // ambiguous: leave the dependency untouched
|
||||
found = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
public static boolean bind(Object removeById, KahluaTable visualList) {
|
||||
// In installed MTK 2.0.0 removeByID closes over exactly one value:
|
||||
// projectilesByID. Read that reference; never replace or modify it.
|
||||
if (!(removeById instanceof LuaClosure fn) || fn.prototype.numParams != 2 || fn.upvalues.length != 1
|
||||
|| !Arrays.asList(fn.prototype.constants).containsAll(List.of("id","list","table","remove"))
|
||||
|| fn.upvalues[0] == null || !(fn.upvalues[0].getValue() instanceof KahluaTable table)
|
||||
|| visualList == null) return false;
|
||||
physics = table;
|
||||
visuals = visualList;
|
||||
frames.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void capture(Frame frame) {
|
||||
if (frame == null) return;
|
||||
if (physics == null || visuals == null) { frames.remove(frame.scene); return; }
|
||||
var result = new ArrayList<TracerMesh.Segment>();
|
||||
for (int i = 1, count = Math.min(visuals.len(), LIMIT); i <= count; i++) {
|
||||
if (!(visuals.rawget(i) instanceof KahluaTable visual)) continue;
|
||||
if (!(physics.rawget(visual.rawget("id")) instanceof KahluaTable shot)) continue;
|
||||
double distance = num(shot, "distance"), travelled = num(shot, "currentDistance");
|
||||
double x = num(shot,"startX"), y = num(shot,"startY"), z = num(shot,"startHeight");
|
||||
double ex = num(shot,"endX"), ey = num(shot,"endY"), ez = num(shot,"endHeight");
|
||||
if (!finite(distance,travelled,x,y,z,ex,ey,ez) || distance <= 0 || travelled <= 0 || travelled >= distance) continue;
|
||||
double scale = num(visual,"scale");
|
||||
if (!Double.isFinite(scale)) scale = 0.1;
|
||||
double length = Math.max(0.45, Math.min(2.5, distance * Math.max(0.1,scale) * 0.08));
|
||||
double head = Math.min(1,travelled/distance), tail = Math.max(0,(travelled-length)/distance);
|
||||
double hx=x+(ex-x)*head, hy=y+(ey-y)*head, hz=z+(ez-z)*head;
|
||||
double tx=x+(ex-x)*tail, ty=y+(ey-y)*tail, tz=z+(ez-z)*tail;
|
||||
float[] h=local(frame.scene,hx,hy,hz), t=local(frame.scene,tx,ty,tz);
|
||||
KahluaTable color = visual.rawget("color") instanceof KahluaTable c ? c : null;
|
||||
result.add(new TracerMesh.Segment(h[0],h[1],h[2],t[0],t[1],t[2],
|
||||
color(color,1,1),color(color,2,0.72f),color(color,3,0.14f),1));
|
||||
}
|
||||
frames.put(frame.scene,List.copyOf(result));
|
||||
}
|
||||
|
||||
static List<TracerMesh.Segment> segments(SceneData scene) {
|
||||
return frames.getOrDefault(scene,List.of());
|
||||
}
|
||||
|
||||
static float[] local(SceneData scene, double x, double y, double z) {
|
||||
double lift=scene.relief == null ? 0 : scene.relief.scene(x,y);
|
||||
return new float[]{(float)(-x-scene.originX),(float)(z*LEVEL+lift-scene.originY),(float)(-y-scene.originZ)};
|
||||
}
|
||||
|
||||
private static double num(KahluaTable t,String key) {
|
||||
return t.rawget(key) instanceof Number n ? n.doubleValue() : Double.NaN;
|
||||
}
|
||||
private static float color(KahluaTable t,int key,float fallback) {
|
||||
return t != null && t.rawget(key) instanceof Number n && Float.isFinite(n.floatValue())
|
||||
? Math.max(0,Math.min(1,n.floatValue())) : fallback;
|
||||
}
|
||||
private static boolean finite(double... values) {
|
||||
for (double v:values) if (!Double.isFinite(v)) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package localmods.mtkviewpoint;
|
||||
|
||||
import me.zed_0xff.zombie_buddy.Patch;
|
||||
import viewpoint.core.Frame;
|
||||
import viewpoint.render.SceneData;
|
||||
|
||||
public final class RenderPatches {
|
||||
private RenderPatches() {}
|
||||
|
||||
@Patch(className="viewpoint.FP",methodName="captureWorld")
|
||||
public static class Snapshot {
|
||||
@Patch.OnExit
|
||||
public static void exit(@Patch.Argument(0) Frame frame) {
|
||||
try { ProjectileBridge.capture(frame); }
|
||||
catch (Throwable error) { TracerRenderer.fail(error); }
|
||||
}
|
||||
}
|
||||
|
||||
@Patch(className="viewpoint.render.WorldRenderer",methodName="drawWeather")
|
||||
public static class Draw {
|
||||
@Patch.OnExit
|
||||
public static void exit(@Patch.Argument(0) SceneData scene) {
|
||||
TracerRenderer.draw(scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package localmods.mtkviewpoint;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Projected ribbons retain world depth; clipping prevents flashes behind the camera. */
|
||||
public final class TracerMesh {
|
||||
public record Segment(float hx,float hy,float hz,float tx,float ty,float tz,float r,float g,float b,float alpha) {}
|
||||
public static final int STRIDE=10;
|
||||
private TracerMesh() {}
|
||||
|
||||
public static int append(FloatBuffer out, Segment s, Matrix4f vp, int width, int height) {
|
||||
if (width<=0 || height<=0 || out.remaining()<6*STRIDE) return 0;
|
||||
var h=vp.transform(new Vector4f(s.hx,s.hy,s.hz,1));
|
||||
var t=vp.transform(new Vector4f(s.tx,s.ty,s.tz,1));
|
||||
if (!h.isFinite() || !t.isFinite()) return 0;
|
||||
float hn=h.z+h.w, tn=t.z+t.w;
|
||||
if (hn<=0 && tn<=0) return 0;
|
||||
if (hn<=0) h.lerp(t,(-hn+0.00001f)/(tn-hn));
|
||||
else if (tn<=0) t.lerp(h,(-tn+0.00001f)/(hn-tn));
|
||||
if (h.w<=0.000001f || t.w<=0.000001f) return 0;
|
||||
float dx=(h.x/h.w-t.x/t.w)*width, dy=(h.y/h.w-t.y/t.w)*height;
|
||||
float len=(float)Math.hypot(dx,dy);
|
||||
if (len<1) { // End-on rounds still have a small visible core.
|
||||
t.x-=4f/width*t.w; dx=4; dy=0; len=4;
|
||||
}
|
||||
float ox=-dy/len*2.5f/width, oy=dx/len*2.5f/height;
|
||||
vertex(out,t,ox,oy,-1,0,s); vertex(out,h,ox,oy,-1,1,s); vertex(out,h,ox,oy,1,1,s);
|
||||
vertex(out,t,ox,oy,-1,0,s); vertex(out,h,ox,oy,1,1,s); vertex(out,t,ox,oy,1,0,s);
|
||||
return 6;
|
||||
}
|
||||
|
||||
private static void vertex(FloatBuffer b,Vector4f p,float ox,float oy,float side,float along,Segment s) {
|
||||
b.put(p.x+ox*side*p.w).put(p.y+oy*side*p.w).put(p.z).put(p.w);
|
||||
b.put(along).put(side).put(s.r).put(s.g).put(s.b).put(s.alpha);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package localmods.mtkviewpoint;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.FloatBuffer;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import static org.lwjgl.opengl.GL33.*;
|
||||
import viewpoint.render.FrameContext;
|
||||
import viewpoint.render.SceneData;
|
||||
import viewpoint.render.WorldRenderer;
|
||||
|
||||
/** Native Viewpoint HDR pass, before post processing, with normal scene depth testing. */
|
||||
public final class TracerRenderer {
|
||||
private static final FloatBuffer vertices=BufferUtils.createFloatBuffer(ProjectileBridge.LIMIT*6*TracerMesh.STRIDE);
|
||||
private static Field frameField,irisField;
|
||||
private static int program,vao,vbo;
|
||||
private static volatile boolean failed;
|
||||
private TracerRenderer() {}
|
||||
|
||||
public static void fail(Throwable error) {
|
||||
if (!failed) System.out.println("[MTKViewpointTurrets] 3D tracers disabled after render error: "+error);
|
||||
failed=true;
|
||||
}
|
||||
|
||||
public static void draw(SceneData scene) {
|
||||
if (failed || scene==null) return;
|
||||
var shots=ProjectileBridge.segments(scene);
|
||||
if (shots.isEmpty()) return;
|
||||
try {
|
||||
if (frameField==null) {
|
||||
frameField=WorldRenderer.class.getDeclaredField("frame"); frameField.setAccessible(true);
|
||||
irisField=WorldRenderer.class.getDeclaredField("irisDrawn"); irisField.setAccessible(true);
|
||||
}
|
||||
if (irisField.getBoolean(null)) return; // Minecraft/Iris uses separate private targets.
|
||||
FrameContext frame=(FrameContext)frameField.get(null);
|
||||
if (frame==null || frame.scene!=scene) return;
|
||||
vertices.clear();
|
||||
int count=0;
|
||||
for (var shot:shots) count+=TracerMesh.append(vertices,shot,frame.viewProjection,frame.width,frame.height);
|
||||
vertices.flip();
|
||||
if (count==0) return;
|
||||
drawBuffer(vertices,count,frame.width,frame.height,true);
|
||||
} catch (Throwable error) { fail(error); }
|
||||
}
|
||||
|
||||
// Kept separate so the same production GPU path can be checked in a tiny
|
||||
// hidden OpenGL test context without launching Project Zomboid.
|
||||
static void drawBuffer(FloatBuffer data,int count,int width,int height,boolean bindViewpointTarget) {
|
||||
int oldProgram=glGetInteger(GL_CURRENT_PROGRAM),oldVAO=glGetInteger(GL_VERTEX_ARRAY_BINDING);
|
||||
int oldVBO=glGetInteger(GL_ARRAY_BUFFER_BINDING),oldDraw=glGetInteger(GL_DRAW_FRAMEBUFFER_BINDING);
|
||||
int oldRead=glGetInteger(GL_READ_FRAMEBUFFER_BINDING),depthFunc=glGetInteger(GL_DEPTH_FUNC);
|
||||
int[] viewport=new int[4]; glGetIntegerv(GL_VIEWPORT,viewport);
|
||||
boolean depth=glIsEnabled(GL_DEPTH_TEST),blend=glIsEnabled(GL_BLEND),cull=glIsEnabled(GL_CULL_FACE);
|
||||
boolean scissor=glIsEnabled(GL_SCISSOR_TEST),depthMask=glGetBoolean(GL_DEPTH_WRITEMASK);
|
||||
int sr=glGetInteger(GL_BLEND_SRC_RGB),dr=glGetInteger(GL_BLEND_DST_RGB);
|
||||
int sa=glGetInteger(GL_BLEND_SRC_ALPHA),da=glGetInteger(GL_BLEND_DST_ALPHA);
|
||||
int er=glGetInteger(GL_BLEND_EQUATION_RGB),ea=glGetInteger(GL_BLEND_EQUATION_ALPHA);
|
||||
try {
|
||||
if (program==0) init();
|
||||
if (bindViewpointTarget) WorldRenderer.bindTarget(); else glViewport(0,0,width,height);
|
||||
glUseProgram(program); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER,vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER,data,GL_STREAM_DRAW);
|
||||
glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDepthMask(false);
|
||||
glDisable(GL_CULL_FACE); glDisable(GL_SCISSOR_TEST); glEnable(GL_BLEND);
|
||||
glBlendEquationSeparate(GL_FUNC_ADD,GL_FUNC_ADD);
|
||||
glBlendFuncSeparate(GL_ONE,GL_ONE_MINUS_SRC_ALPHA,GL_ZERO,GL_ONE);
|
||||
glDrawArrays(GL_TRIANGLES,0,count);
|
||||
} finally {
|
||||
glUseProgram(oldProgram); glBindVertexArray(oldVAO); glBindBuffer(GL_ARRAY_BUFFER,oldVBO);
|
||||
glBlendEquationSeparate(er,ea); glBlendFuncSeparate(sr,dr,sa,da);
|
||||
toggle(GL_BLEND,blend); toggle(GL_DEPTH_TEST,depth); toggle(GL_CULL_FACE,cull); toggle(GL_SCISSOR_TEST,scissor);
|
||||
glDepthFunc(depthFunc); glDepthMask(depthMask);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER,oldDraw); glBindFramebuffer(GL_READ_FRAMEBUFFER,oldRead);
|
||||
glViewport(viewport[0],viewport[1],viewport[2],viewport[3]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void toggle(int cap,boolean on) { if(on) glEnable(cap); else glDisable(cap); }
|
||||
|
||||
private static void init() {
|
||||
int vs=shader(GL_VERTEX_SHADER,"""
|
||||
#version 330 core
|
||||
layout(location=0) in vec4 clipPosition;
|
||||
layout(location=1) in vec2 uv;
|
||||
layout(location=2) in vec4 color;
|
||||
out vec2 texCoord;
|
||||
out vec4 tint;
|
||||
void main(){gl_Position=clipPosition; texCoord=uv; tint=color;}
|
||||
""");
|
||||
int fs=shader(GL_FRAGMENT_SHADER,"""
|
||||
#version 330 core
|
||||
in vec2 texCoord;
|
||||
in vec4 tint;
|
||||
out vec4 fragColor;
|
||||
void main(){
|
||||
float edge=1.0-smoothstep(0.25,1.0,abs(texCoord.y));
|
||||
float a=tint.a*edge*smoothstep(0.0,0.25,texCoord.x);
|
||||
fragColor=vec4(tint.rgb*5.0*a,a);
|
||||
}
|
||||
""");
|
||||
int linked=glCreateProgram();
|
||||
try {
|
||||
glAttachShader(linked,vs); glAttachShader(linked,fs); glLinkProgram(linked);
|
||||
if(glGetProgrami(linked,GL_LINK_STATUS)==0) throw new IllegalStateException(glGetProgramInfoLog(linked));
|
||||
} catch(RuntimeException error) {glDeleteProgram(linked); throw error;}
|
||||
finally {glDeleteShader(vs); glDeleteShader(fs);}
|
||||
vao=glGenVertexArrays(); vbo=glGenBuffers();
|
||||
glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER,vbo);
|
||||
int stride=TracerMesh.STRIDE*Float.BYTES;
|
||||
glEnableVertexAttribArray(0); glVertexAttribPointer(0,4,GL_FLOAT,false,stride,0L);
|
||||
glEnableVertexAttribArray(1); glVertexAttribPointer(1,2,GL_FLOAT,false,stride,16L);
|
||||
glEnableVertexAttribArray(2); glVertexAttribPointer(2,4,GL_FLOAT,false,stride,24L);
|
||||
program=linked;
|
||||
}
|
||||
private static int shader(int type,String source) {
|
||||
int id=glCreateShader(type); glShaderSource(id,source); glCompileShader(id);
|
||||
if(glGetShaderi(id,GL_COMPILE_STATUS)==0) {
|
||||
String error=glGetShaderInfoLog(id); glDeleteShader(id); throw new IllegalStateException(error);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user