Make JavaFX Image Nearest Neighbor (every frame, multiple times per frame)

I'm making a voxel game with JavaFX as a base. No, I'm not using PerspectiveCamera, I'm making my own engine. The issue is that javafx makes images smoothed when they are stretched, which is ABSOLUTELY STUPID AND I HATE IT. You see, voxel games have low resolution textures (something like 16x16), and when you get up close and personal with a block, that could be stretched to thousands of pixels if you have a good monitor. I have tried: - Changing requested width and height in image constructor (this kinda works, but memory inefficient) - Setting fit width and height in image view - Looking through javafx.scene.effects to see if there's an effect to set the smoothing to 0 - Asking chatgpt - Searching for about 5 hours on google - Using Canvas (PerspectiveTransform doesn't work, so no good) - Using java swing BufferedImage - Looking in the javafx code Some things that would work but are too slow to be run many times per frame: - Pixel Reader/Writer resizing - Loading image from disk with different RW+H - Loading image from disk to byte[], then using ByteArrayInputStream - Processing every pixel with canvas I feel like I forgot one or two things in those lists, but those are enough things for you to (hopefully) understand my frustration. I don't really want to use external libraries as I want no ties to anything other than JavaFX, but if there is no other way, I can do it. After writing this, I feel like I forgot to clarify what I mean by "smoothing". Image 1 is what I want it to look like (done with RW+H, but that's inefficient), and image 2 is what it looks like normally.
No description
No description
89 Replies
JavaBot
JavaBot12mo ago
This post has been reserved for your question.
Hey @The Typhothanian! Please use /close or the Close Post button above when your problem is solved. Please remember to follow the help guidelines. This post will be automatically closed after 300 minutes of inactivity.
TIP: Narrow down your issue to simple and precise questions to maximize the chance that others will reply in here.
The Typhothanian
The TyphothanianOP12mo ago
Yes, I know that the textures are rotated wrong and are on the wrong faces, I'll fix that later.
tjoener
tjoener12mo ago
What are you rendering with, trianglemesh? and phongshading?
The Typhothanian
The TyphothanianOP12mo ago
Neither, as I said, I am using my own engine That's adding an image view and setting the effect of the IV to a PerspectiveTransform
Unknown User
Unknown User12mo ago
Message Not Public
Sign In & Join Server To View
The Typhothanian
The TyphothanianOP12mo ago
How do I do that?
tjoener
tjoener12mo ago
Sooo, using Graphics(2D)?
The Typhothanian
The TyphothanianOP12mo ago
Wdym GraphicsContext2D? If so, then no. Hello?
tjoener
tjoener12mo ago
Show me some drawing code So I can figure it out lol
The Typhothanian
The TyphothanianOP12mo ago
K Projection code:
package net.typho.pnegative.rendering;

import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import net.typho.pnegative.PNegative;

import static java.lang.Math.*;
import static net.typho.pnegative.PNegative.camera;

public class Projection {
public static Matrix perspective = new Matrix(), view = new Matrix();
public static double fov = 120, near = 0, far = 5000;

public static void update() {
double cos = cos(toRadians(camera.rotX())),
sin = sin(toRadians(camera.rotX()));
view.data = new double[][]{ // rot x
{1, 0, 0, 0},
{0, cos, -sin, 0},
{0, sin, cos, 0},
{0, 0, 0, 1}
};

cos = cos(toRadians(camera.rotY()));
sin = sin(toRadians(camera.rotY()));
view.mul(new double[][]{ // rot y
{cos, 0, sin, 0},
{0, 1, 0, 0},
{-sin, 0, cos, 0},
{0, 0, 0, 1}
});

cos = cos(toRadians(camera.rotZ()));
sin = sin(toRadians(camera.rotZ()));
view.mul(new double[][]{ // rot z
{cos, -sin, 0, 0},
{sin, cos, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1}
});

view.mul(new double[][]{ // pos
{1, 0, 0, -camera.posX()},
{0, 1, 0, -camera.posY()},
{0, 0, 1, -camera.posZ()},
{0, 0, 0, 1}
});

double f = 1 / tan(toRadians(fov) / 2);
perspective.data = new double[][]{
{f / (PNegative.width.get() / PNegative.height.get()), 0, 0, 0},
{0, f, 0, 0},
{0, 0, (far + near) / (near - far), (2 * far * near) / (near - far)},
{0, 0, -1, 0}
};
perspective.mul(view.data);
}
}
package net.typho.pnegative.rendering;

import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import net.typho.pnegative.PNegative;

import static java.lang.Math.*;
import static net.typho.pnegative.PNegative.camera;

public class Projection {
public static Matrix perspective = new Matrix(), view = new Matrix();
public static double fov = 120, near = 0, far = 5000;

public static void update() {
double cos = cos(toRadians(camera.rotX())),
sin = sin(toRadians(camera.rotX()));
view.data = new double[][]{ // rot x
{1, 0, 0, 0},
{0, cos, -sin, 0},
{0, sin, cos, 0},
{0, 0, 0, 1}
};

cos = cos(toRadians(camera.rotY()));
sin = sin(toRadians(camera.rotY()));
view.mul(new double[][]{ // rot y
{cos, 0, sin, 0},
{0, 1, 0, 0},
{-sin, 0, cos, 0},
{0, 0, 0, 1}
});

cos = cos(toRadians(camera.rotZ()));
sin = sin(toRadians(camera.rotZ()));
view.mul(new double[][]{ // rot z
{cos, -sin, 0, 0},
{sin, cos, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1}
});

view.mul(new double[][]{ // pos
{1, 0, 0, -camera.posX()},
{0, 1, 0, -camera.posY()},
{0, 0, 1, -camera.posZ()},
{0, 0, 0, 1}
});

double f = 1 / tan(toRadians(fov) / 2);
perspective.data = new double[][]{
{f / (PNegative.width.get() / PNegative.height.get()), 0, 0, 0},
{0, f, 0, 0},
{0, 0, (far + near) / (near - far), (2 * far * near) / (near - far)},
{0, 0, -1, 0}
};
perspective.mul(view.data);
}
}
public static Point2D project(Point3D point) {
double[] homogeneousPoint = {point.getX(), point.getY(), point.getZ(), 1};
double[] projectedPoint = new double[4];

for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
projectedPoint[i] += perspective.data[i][j] * homogeneousPoint[j];
}
}

if (projectedPoint[3] != 0) {
projectedPoint[0] /= projectedPoint[3];
projectedPoint[1] /= projectedPoint[3];
}

return new Point2D((projectedPoint[0] + 0.5) * PNegative.width.get(), (projectedPoint[1] + 0.5) * PNegative.height.get());
}

public static class Matrix {
private double[][] data;

Matrix() {
identity();
}

public void mul(double[][] matrix) {
int rows = this.data.length;
int cols = matrix[0].length;
int common = this.data[0].length;
double[][] newMatrix = new double[rows][common];

for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
for (int k = 0; k < common; k++) {
newMatrix[i][j] += this.data[i][k] * matrix[k][j];
}
}
}

this.data = newMatrix;
}

public void identity() {
data = new double[][]{
{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1}
};
}
}
public static Point2D project(Point3D point) {
double[] homogeneousPoint = {point.getX(), point.getY(), point.getZ(), 1};
double[] projectedPoint = new double[4];

for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
projectedPoint[i] += perspective.data[i][j] * homogeneousPoint[j];
}
}

if (projectedPoint[3] != 0) {
projectedPoint[0] /= projectedPoint[3];
projectedPoint[1] /= projectedPoint[3];
}

return new Point2D((projectedPoint[0] + 0.5) * PNegative.width.get(), (projectedPoint[1] + 0.5) * PNegative.height.get());
}

public static class Matrix {
private double[][] data;

Matrix() {
identity();
}

public void mul(double[][] matrix) {
int rows = this.data.length;
int cols = matrix[0].length;
int common = this.data[0].length;
double[][] newMatrix = new double[rows][common];

for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
for (int k = 0; k < common; k++) {
newMatrix[i][j] += this.data[i][k] * matrix[k][j];
}
}
}

this.data = newMatrix;
}

public void identity() {
data = new double[][]{
{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1}
};
}
}
tjoener
tjoener12mo ago
if you format them, after the 3 backticks, type java so you have syntax highlighting
The Typhothanian
The TyphothanianOP12mo ago
Like how? just put java after the backticks?
tjoener
tjoener12mo ago
yeah You can even just edit your pasted code much better 😉
The Typhothanian
The TyphothanianOP12mo ago
There So, that is the projection code
tjoener
tjoener12mo ago
Yeah seems normal
The Typhothanian
The TyphothanianOP12mo ago
I just call the project method to get 3d to 2d
tjoener
tjoener12mo ago
Where's the drawing code?
The Typhothanian
The TyphothanianOP12mo ago
That is approximately 500 lines and is not needed for this, so I'll just explain the important parts
tjoener
tjoener12mo ago
yeah, but your projection has nothing to do with the smoothing... That's all in the drawing code import static java.lang.Math.*; also gross lol 😄 I hate static imports
The Typhothanian
The TyphothanianOP12mo ago
Well I don't have the old simplified version I have two Math classes one sec im typing I keep pasting but it becomes a text file, that okay?
tjoener
tjoener12mo ago
sure
The Typhothanian
The TyphothanianOP12mo ago
Hmm, you can't view that in discord Hold on
tjoener
tjoener12mo ago
Affine (JavaFX 21)
declaration: module: javafx.graphics, package: javafx.scene.transform, class: Affine
tjoener
tjoener12mo ago
You do know that exists?
The Typhothanian
The TyphothanianOP12mo ago
I have absolutely no clue what that is Well, too late for that to help me. Wish I found that two weeks ago It took me two days to make that projection matrix Anyway, here's the important part of the draw code:
default List<Node> draw(Map<Point3D, Point2D> projectedPoints, Point3D[] absolutePoints) {
List<Node> nodes = new ArrayList<>();

int i = 0;
for (int[] face : indices) {
Point2D a = projectedPoints.get(absolutePoints[face[0]]),
b = projectedPoints.get(absolutePoints[face[1]]),
c = projectedPoints.get(absolutePoints[face[2]]),
d = projectedPoints.get(absolutePoints[face[3]]);

if ((a.getX() * b.getY() + b.getX() * c.getY() + c.getX() * d.getY() + d.getX() * a.getY()) -
(b.getX() * a.getY() + c.getX() * b.getY() + d.getX() * c.getY() + a.getX() * d.getY()) < 0) {
nodes.add(new ProjectedFace(
getFace(i, range(a.getX(), b.getX(), c.getX(), d.getX()).doubleValue(), range(a.getY(), b.getY(), c.getY(), d.getY()).doubleValue()),
this,
i,
a, b, c, d
));
}

i++;
}

return nodes;
}
default List<Node> draw(Map<Point3D, Point2D> projectedPoints, Point3D[] absolutePoints) {
List<Node> nodes = new ArrayList<>();

int i = 0;
for (int[] face : indices) {
Point2D a = projectedPoints.get(absolutePoints[face[0]]),
b = projectedPoints.get(absolutePoints[face[1]]),
c = projectedPoints.get(absolutePoints[face[2]]),
d = projectedPoints.get(absolutePoints[face[3]]);

if ((a.getX() * b.getY() + b.getX() * c.getY() + c.getX() * d.getY() + d.getX() * a.getY()) -
(b.getX() * a.getY() + c.getX() * b.getY() + d.getX() * c.getY() + a.getX() * d.getY()) < 0) {
nodes.add(new ProjectedFace(
getFace(i, range(a.getX(), b.getX(), c.getX(), d.getX()).doubleValue(), range(a.getY(), b.getY(), c.getY(), d.getY()).doubleValue()),
this,
i,
a, b, c, d
));
}

i++;
}

return nodes;
}
tjoener
tjoener12mo ago
Time for a refactor 🙂 ProjectedFace Can you send that one?
The Typhothanian
The TyphothanianOP12mo ago
hold on The P3D[] is 8 points, and those absolute points are after it has been moved and rotated and such, don't need that part ProjectedFace is just an ImageView with extra stuff for block placing/breaking:
package net.typho.pnegative.rendering;

import javafx.geometry.Point2D;
import javafx.scene.effect.PerspectiveTransform;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.shape.Polyline;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeLineJoin;
import net.typho.pnegative.world.ModelBox;

public class ProjectedFace extends ImageView {
public final ModelBox box;
public final int ordinal;
public final Point2D[] points;

public ProjectedFace(Image image, ModelBox box, int ordinal, Point2D... points) {
super(image);

this.box = box;
this.ordinal = ordinal;
this.points = points;
setEffect(
new PerspectiveTransform(
points[2].getX(), points[2].getY(),
points[3].getX(), points[3].getY(),
points[0].getX(), points[0].getY(),
points[1].getX(), points[1].getY()
)
);
}

public Polyline getOutline() {
return new Polyline(
points[0].getX(), points[0].getY(),
points[1].getX(), points[1].getY(),
points[2].getX(), points[2].getY(),
points[3].getX(), points[3].getY(),
points[0].getX(), points[0].getY()
) {{
setStrokeWidth(5);
setStrokeLineJoin(StrokeLineJoin.ROUND);
}};
}
}
package net.typho.pnegative.rendering;

import javafx.geometry.Point2D;
import javafx.scene.effect.PerspectiveTransform;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.shape.Polyline;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeLineJoin;
import net.typho.pnegative.world.ModelBox;

public class ProjectedFace extends ImageView {
public final ModelBox box;
public final int ordinal;
public final Point2D[] points;

public ProjectedFace(Image image, ModelBox box, int ordinal, Point2D... points) {
super(image);

this.box = box;
this.ordinal = ordinal;
this.points = points;
setEffect(
new PerspectiveTransform(
points[2].getX(), points[2].getY(),
points[3].getX(), points[3].getY(),
points[0].getX(), points[0].getY(),
points[1].getX(), points[1].getY()
)
);
}

public Polyline getOutline() {
return new Polyline(
points[0].getX(), points[0].getY(),
points[1].getX(), points[1].getY(),
points[2].getX(), points[2].getY(),
points[3].getX(), points[3].getY(),
points[0].getX(), points[0].getY()
) {{
setStrokeWidth(5);
setStrokeLineJoin(StrokeLineJoin.ROUND);
}};
}
}
Ignore the getOutline method
tjoener
tjoener12mo ago
aaah, that's an imageview ok
The Typhothanian
The TyphothanianOP12mo ago
I have no clue why I have to do 2, 3, 0, 1 instead of 0, 1, 2, 3, but whatever
tjoener
tjoener12mo ago
ok, in the constructor of projectedface add setSmooth(false); And see if that works
The Typhothanian
The TyphothanianOP12mo ago
Tried that didnt work
tjoener
tjoener12mo ago
What does that do?
The Typhothanian
The TyphothanianOP12mo ago
setSmooth?
tjoener
tjoener12mo ago
yeah, what does the output look like if you do that
The Typhothanian
The TyphothanianOP12mo ago
No difference for true or false At least, not that I can see Ah yes I have claimed the 69th message (this is it)
tjoener
tjoener12mo ago
Yeah, unfortunately imageview does not allow nearest neighbor filtering
The Typhothanian
The TyphothanianOP12mo ago
And that's stupid Do you know what method smooths it, so I can override it in my projected face and be done?
tjoener
tjoener12mo ago
None really, the property sets something internal in the rendering engine
The Typhothanian
The TyphothanianOP12mo ago
UGH
tjoener
tjoener12mo ago
lemme check rq how it works
The Typhothanian
The TyphothanianOP12mo ago
Yeah, back when I was trying out the PerspectiveCamera thingy, I got around this by setting the diffuse color to black and setting the SIM to the image. That screwed up lighting but I wasn't planning on using the built-in light engine anyway Then I decided to make my own engine and have it super optimized for my game
tjoener
tjoener12mo ago
My idea, grab a canvas, grab it's graphics
@Override
protected void renderContent(Graphics g) {
int imgW = image.getWidth();
int imgH = image.getHeight();

ResourceFactory factory = g.getResourceFactory();
int maxSize = maxSizeWrapper(factory);
if (imgW <= maxSize && imgH <= maxSize) {
Texture texture = factory.getCachedTexture(image, Texture.WrapMode.CLAMP_TO_EDGE);
if (coords == null) {
g.drawTexture(texture, x, y, x + w, y + h, 0, 0, imgW, imgH);
} else {
coords.draw(texture, g, x, y);
}
texture.unlock();
} else {
if (compoundImage == null) compoundImage = new CachingCompoundImage(image, maxSize);
// coords is null iff there was no viewport specified, but
// MegaCoords needs a non-null Coords so we create a dummy one
if (coords == null) coords = new Coords(w, h, new ViewPort(0, 0, imgW, imgH));
if (compoundCoords == null) compoundCoords = new CompoundCoords(compoundImage, coords);
compoundCoords.draw(g, compoundImage, x, y);
}
}
@Override
protected void renderContent(Graphics g) {
int imgW = image.getWidth();
int imgH = image.getHeight();

ResourceFactory factory = g.getResourceFactory();
int maxSize = maxSizeWrapper(factory);
if (imgW <= maxSize && imgH <= maxSize) {
Texture texture = factory.getCachedTexture(image, Texture.WrapMode.CLAMP_TO_EDGE);
if (coords == null) {
g.drawTexture(texture, x, y, x + w, y + h, 0, 0, imgW, imgH);
} else {
coords.draw(texture, g, x, y);
}
texture.unlock();
} else {
if (compoundImage == null) compoundImage = new CachingCompoundImage(image, maxSize);
// coords is null iff there was no viewport specified, but
// MegaCoords needs a non-null Coords so we create a dummy one
if (coords == null) coords = new Coords(w, h, new ViewPort(0, 0, imgW, imgH));
if (compoundCoords == null) compoundCoords = new CompoundCoords(compoundImage, coords);
compoundCoords.draw(g, compoundImage, x, y);
}
}
This is the full render of the underlying imageview Seems easy enough to just do that drawing by hand
public void draw(Texture t, Graphics g, float x, float y) {
g.drawTexture(t,
x + x0, y + y0, x + x1, y + y1,
u0, v0, u1, v1);
}
public void draw(Texture t, Graphics g, float x, float y) {
g.drawTexture(t,
x + x0, y + y0, x + x1, y + y1,
u0, v0, u1, v1);
}
The Typhothanian
The TyphothanianOP12mo ago
Protected means I can't override it, right? Or is that final?
tjoener
tjoener12mo ago
And this is the draw for that coords thing That's in a different class NGImageView, can't get to that
The Typhothanian
The TyphothanianOP12mo ago
So the render content is in IV?
tjoener
tjoener12mo ago
nope, NGImageView
The Typhothanian
The TyphothanianOP12mo ago
absolute displeasure
tjoener
tjoener12mo ago
Yeah, image views are not meant to render textures Plus you're only getting an affine transform
The Typhothanian
The TyphothanianOP12mo ago
And the draw method is where?
tjoener
tjoener12mo ago
I believe that will give you PS1 textures 🙂 that's what coords.draw does in the first listing
The Typhothanian
The TyphothanianOP12mo ago
First listing?
tjoener
tjoener12mo ago
in rendercontent
The Typhothanian
The TyphothanianOP12mo ago
Okay, I'm getting confused. What are you suggesting again? I got 26 min then I have to go
tjoener
tjoener12mo ago
So, grab a canvas, grab it's graphics, and do the draw manually
The Typhothanian
The TyphothanianOP12mo ago
GraphicsContext2D?
tjoener
tjoener12mo ago
Although that might not be enough I think if you really wan't a 3d engine, javafx is not going to cut it Unless you're happy with the smoothing
The Typhothanian
The TyphothanianOP12mo ago
Oh, it is going to cut it as LWJGL is impossible to work with Lets see...
tjoener
tjoener12mo ago
lwjgl is just opengl, it's not that hard
The Typhothanian
The TyphothanianOP12mo ago
It is if there's no tutorials at all
tjoener
tjoener12mo ago
There's always the trick of just scaling your image 16 times with nearest neighbor in a photo editor and using that the smoothing will be less as you already start from a larger image, so less scaling is needed
The Typhothanian
The TyphothanianOP12mo ago
Yeah, but 1. Takes up too much memory and 2. can still be blurry when close up
tjoener
tjoener12mo ago
yeah
The Typhothanian
The TyphothanianOP12mo ago
And I can do that in image constructor Hold on, lemme implement the canvas thing and i'll get back to you
tjoener
tjoener12mo ago
It's more for 2d drawing
The Typhothanian
The TyphothanianOP12mo ago
whatevs What's this ImageObserver thing I'm seeing in Graphics.drawImage? tjoener? nvmd was using wrong class I have that implemented, but I'm just seeing a white screen?
tjoener
tjoener12mo ago
should render fine, I've done canvas before Might need to reposition things
The Typhothanian
The TyphothanianOP12mo ago
hmm I'm stupid, the canvas is 0 by 0 ARGH the canvas gc doesn't have x1 y1 to x2 y2, it has x y w h And I have no clue how to do the transform thing
tjoener
tjoener12mo ago
like I said, might not be the best tool for the job
The Typhothanian
The TyphothanianOP12mo ago
Hmm Well, in LWJGL, I had the exact same issue. Do you know how Minecraft fixes it? And I gtg in 2 min
tjoener
tjoener12mo ago
with the fuzzy rendering?
The Typhothanian
The TyphothanianOP12mo ago
Yeah Maybe it could give me a nudge in the right direction
tjoener
tjoener12mo ago
Yeah, there's a shader calculation you have to make
The Typhothanian
The TyphothanianOP12mo ago
oh great
tjoener
tjoener12mo ago
or GL_NEAREST but that might not be enough
The Typhothanian
The TyphothanianOP12mo ago
Welp, I know exactly what I'm doing tomorrow. Figuring out an efficient algorithm to replace PT. It feels like only two weeks ago I had to make a complex mathematical program for my game Oh wait, it was two weeks ago Well, gtg. ill post the code here once i figure it out, dont close the thread
JavaBot
JavaBot12mo ago
If you are finished with your post, please close it. If you are not, please ignore this message. Note that you will not be able to send further messages here after this post have been closed but you will be able to create new posts.
The Typhothanian
The TyphothanianOP12mo ago
uh huh i know
tjoener
tjoener12mo ago
Yeah, if you thought you could do game programming, or writing your own engine with only a little math, boy do I have news for you
JavaBot
JavaBot12mo ago
💤 Post marked as dormant
This post has been inactive for over 300 minutes, thus, it has been archived. If your question was not answered yet, feel free to re-open this post or create a new one. In case your post is not getting any attention, you can try to use /help ping. Warning: abusing this will result in moderative actions taken against you.
The Typhothanian
The TyphothanianOP12mo ago
I'm almost there
No description
The Typhothanian
The TyphothanianOP12mo ago
I tried canvas but it was too laggy for just two blocks
The Typhothanian
The TyphothanianOP12mo ago
I cannot express my supreme amount of disappointment.
No description
The Typhothanian
The TyphothanianOP12mo ago
@tjoener Do you know how I would go about making my own window manager? I just need mouse and keyboard input, and direct access to each pixel on the screen.
tjoener
tjoener12mo ago
eeeeh I have no idea what you're actually trying to make
The Typhothanian
The TyphothanianOP12mo ago
Me neither But, like I said, the canvas is too laggy, so I want to try making something like GLFW. Screw it I'm using GLFW
tjoener
tjoener12mo ago
use lwjgl then at least
Unknown User
Unknown User12mo ago
Message Not Public
Sign In & Join Server To View
JavaBot
JavaBot12mo ago
💤 Post marked as dormant
This post has been inactive for over 300 minutes, thus, it has been archived. If your question was not answered yet, feel free to re-open this post or create a new one. In case your post is not getting any attention, you can try to use /help ping. Warning: abusing this will result in moderative actions taken against you.
Want results from more Discord servers?
Add your server