(April 2015) Monthly WIP Screenshot Thread

@nehon: First of all, great work this is likely to be one of the most highly anticipated features I’m sure. Awesome model too, not sure if you made it or someone else, but that tank is pretty bad ass!

Now is this implementation of PBR similar to what’s being done with Blender’s Cycles render engine? Would it be possible to practice and prototype materials with Blender Cycles and then use the knowledge gained from that experience with this implementation?

Thanks a lot. I did it myself yes.

Actually no. This implementation is pretty similar to what you’d have in UE4 or Unity, to some extend. Except it supports both of the common PBR workflows (metallic/roughness, specular/gloss).

I didn’t look much into blender cycle, but to have PBR with it you need a pretty complex node graph (blender cycle is not meant for real time, whereas all PBR implementations around are). Cycle uses real GI while PBR fake GI with image based lighting with an env map.

That’s where Substance Painter makes sense actually. Because it has a real time PBR renderer, and you can have a (almost) seamless rendering between it and JME.
I say Almost because I still have differences that I’m investigating.

The blender team plans to have a PBR viewport in future versions. I guess it will help a lot too. But IMO cycle is not really suited for the task.

Good to know. I read the series of blog posts about PBR and I imagine I shouldn’t have any trouble working with it at this point, but I do wonder if the current non-PBR render engine will be sticking around? As cool as PBR is I rather doubt my ageing laptop will be able to handle it.

This old clunker has served me well throughout the years, but I must admit I’ve got’er capabilities stretched pretty thin as it is.

I am told that the current lighting model will still work. :+1:

well it’s not much more greedy than the previous lighting model. but a bit.
And yes the previous lighting system will stick around.

Looks great !
@Tryder : How did you made those territory lines ? looks really nice.

The short answer would be that I’m creating a mesh along a bezier curve. I’m using this method in a couple of areas such as movement boundaries, attack boundaries, terrain boundaries and a slightly different mesh for the move path indicator in which the end does not reconnect with the beginning and the UVs are calculated differently to allow for a scrolling animation.

Creating the mesh is the easy part, creating the spline along which the mesh is created is considerably more complicated for the faction boundaries. The creation of faction boundaries is spawned off in a separate thread to prevent hiccups in the frame rate, after the mesh is created it’s queued up and added to the scene.

Long story short I iterate through a particular faction’s space stations looking for the top most station then look for the top most node(grid space) within that station’s resource collection range and begin looping over the boundary using a list of all nodes owned by this faction. I check to see which space station owns the node I’m currently adding to the boundary and remove it from a list of stations that have not been checked. Once I reach the end I rinse and repeat using the list of stations I was removing from so I don’t leave out any space stations whose boundaries are disconnected from others.

once I have the external boundaries completed and stored in a list of boundaries I iterate through the interior of those boundaries looking for holes and use a similar method to loop over the edges of those holes creating interior boundaries. Once that is done I iterate over those interior boundaries again looking for an occurrence where a node in the inner boundary is connected to another part of the boundary via a single node and separate them into two inner boundaries because it looks weird otherwise :slight_smile:

After all of that is done I iterate over all of the boundaries creating lists of control points, splines, based on several factors then pass those splines into a custom mesh class that creates a mesh along the spline.

Here’s the two bezier mesh classes I have right now. They weren’t really developed with versatility in mind so they really only work with a 2D spline that goes along the x/z-axis with normals pointing up along the y-axis.

Cyclical bezier:

public class BezierPathCyclic extends Mesh {
    private final Spline spline;
    private final float width1;
    private final float width2;
    
    public BezierPathCyclic(Spline spline, float width1, float width2) {
        super();
        this.spline = spline;
        this.width1 = width1;
        this.width2 = width2;
        create();
    }
    
    private void create() {
        int numSegments = 32;
        Vector3f tmp = new Vector3f();
        Vector3f tmp2 = new Vector3f();
        Node node = new Node("Path Builder");
        new Node().attachChild(node);
        
        int numPoints = (spline.getControlPoints().size() + 2) / 3;
        int currentControlPoint = 0;
        
        float[] verts = new float[(((numPoints - 1) * numSegments + 1) * 3) * 2];
        float[] normals = new float[verts.length];
        float[] texCoord = new float[(((numPoints - 1) * numSegments + 1) * 2) * 2];
        float[] texCoord2 = new float[texCoord.length];
        int index = 0;
        int nIndex = 0;
        int uvIndex = 0;
        int uvIndex2 = 0;
        float uvY = 0f;
        float uvYSize = 0.04f;
        
        float gameWidth = UI.getGame().getWidth() * CDNode.SIZE;
        float gameHeight = UI.getGame().getHeight() * CDNode.SIZE;
        for (int i = 0; i < numPoints - 1; i++) {
            if (i == 0) {
                spline.interpolate(1f / numSegments, 0, tmp);
                node.setLocalTranslation(spline.getControlPoints().get(currentControlPoint));
                node.lookAt(tmp, Vector3f.UNIT_Y);
                
                node.localToWorld(new Vector3f(width1, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;

                node.localToWorld(new Vector3f(-width2, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;
                
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                
                texCoord[uvIndex++] = 0;
                texCoord[uvIndex++] = 0;
                texCoord[uvIndex++] = 1;
                texCoord[uvIndex++] = 0;
                
                texCoord2[uvIndex2++] = 1f - (verts[index - 6] / gameWidth);
                texCoord2[uvIndex2++] = verts[index - 4] / gameHeight;
                texCoord2[uvIndex2++] = 1f - (verts[index - 3] / gameWidth);
                texCoord2[uvIndex2++] = verts[index - 1] / gameHeight;
            } else {
                tmp = node.getLocalTranslation().clone();
                node.setLocalTranslation(spline.getControlPoints().get(currentControlPoint));
                node.lookAt(tmp, Vector3f.UNIT_Y);
                
                node.localToWorld(new Vector3f(-width1, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;

                node.localToWorld(new Vector3f(width2, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;
                
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                
                uvY += tmp.distance(node.getLocalTranslation()) * uvYSize;
                texCoord[uvIndex++] = 0;
                texCoord[uvIndex++] = uvY;
                texCoord[uvIndex++] = 1;
                texCoord[uvIndex++] = uvY;
                
                texCoord2[uvIndex2++] = 1f - (verts[index - 6] / gameWidth);
                texCoord2[uvIndex2++] = verts[index - 4] / gameHeight;
                texCoord2[uvIndex2++] = 1f - (verts[index - 3] / gameWidth);
                texCoord2[uvIndex2++] = verts[index - 1] / gameHeight;
            }
            
            for (int s = 1; s < numSegments; s++) {
                tmp = node.getLocalTranslation().clone();
                spline.interpolate((float)s / numSegments, currentControlPoint, tmp2);
                node.setLocalTranslation(tmp2);
                node.lookAt(tmp, Vector3f.UNIT_Y);
                
                node.localToWorld(new Vector3f(-width1, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;
                
                node.localToWorld(new Vector3f(width2, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;
                
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                
                uvY += tmp.distance(node.getLocalTranslation()) * uvYSize;
                texCoord[uvIndex++] = 0;
                texCoord[uvIndex++] = uvY;
                texCoord[uvIndex++] = 1;
                texCoord[uvIndex++] = uvY;
                
                texCoord2[uvIndex2++] = 1f - (verts[index - 6] / gameWidth);
                texCoord2[uvIndex2++] = verts[index - 4] / gameHeight;
                texCoord2[uvIndex2++] = 1f - (verts[index - 3] / gameWidth);
                texCoord2[uvIndex2++] = verts[index - 1] / gameHeight;
            }
            
            currentControlPoint += 3;
        }
        tmp = node.getLocalTranslation().clone();
        node.setLocalTranslation(spline.getControlPoints().get(spline.getControlPoints().size() - 1));
        node.lookAt(tmp, Vector3f.UNIT_Y);
        
        verts[index++] = verts[0];
        verts[index++] = 0f;
        verts[index++] = verts[2];

        verts[index++] = verts[3];
        verts[index++] = 0f;
        verts[index++] = verts[5];
        node.removeFromParent();
        
        normals[nIndex++] = 0;
        normals[nIndex++] = 1;
        normals[nIndex++] = 0;
        normals[nIndex++] = 0;
        normals[nIndex++] = 1;
        normals[nIndex++] = 0;
        
        uvY += tmp.distance(node.getLocalTranslation()) * uvYSize;
        texCoord[uvIndex++] = 0;
        texCoord[uvIndex++] = uvY;
        texCoord[uvIndex++] = 1;
        texCoord[uvIndex++] = uvY;
        
        texCoord2[uvIndex2++] = 1f - (verts[index - 6] / gameWidth);
        texCoord2[uvIndex2++] = verts[index - 4] / gameHeight;
        texCoord2[uvIndex2++] = 1f - (verts[index - 3] / gameWidth);
        texCoord2[uvIndex2++] = verts[index - 1] / gameHeight;
        
        short[] indices = new short[((numPoints - 1) * numSegments) * 6];
        for (int ind = 0; ind < (numPoints - 1) * numSegments; ind++) {
            int indInd = ind * 6;
            int indV = ind * 2;
            indices[indInd++] = (short)indV;
            indices[indInd++] = (short)(indV + 1);
            indices[indInd++] = (short)(indV + 2);
            
            indices[indInd++] = (short)(indV + 1);
            indices[indInd++] = (short)(indV + 3);
            indices[indInd] = (short)(indV + 2);
        }
        
        setBuffer(VertexBuffer.Type.Position, 3, verts);
        setBuffer(VertexBuffer.Type.Normal, 3, normals);
        setBuffer(VertexBuffer.Type.TexCoord, 2, texCoord);
        setBuffer(VertexBuffer.Type.TexCoord2, 2, texCoord2);
        setBuffer(VertexBuffer.Type.Index, 3, indices);
        updateBound();
        updateCounts();
    }
}

Non cyclical bezier:

public class BezierPath extends Mesh {
    private final Spline spline;
    
    public BezierPath(Spline spline) {
        super();
        this.spline = spline;
        create();
    }
    
    private void create() {
        int numSegments = 64;
        Vector3f tmp = new Vector3f();
        Vector3f tmp2 = new Vector3f();
        Node node = new Node("Path Builder");
        new Node().attachChild(node);
        
        int numPoints = (spline.getControlPoints().size() + 2) / 3;
        int currentControlPoint = 0;
        
        float[] verts = new float[(((numPoints - 1) * numSegments + 1) * 3) * 2];
        float[] normals = new float[verts.length];
        float[] texCoord = new float[(((numPoints - 1) * numSegments + 1) * 2) * 2];
        float[] texCoord2 = new float[texCoord.length];
        int index = 0;
        int nIndex = 0;
        int uvIndex = 0;
        int uvIndex2 = 0;
        float uvY = 0f;
        float uvYSize = 0.04f;
        float width = 4.5f;
        
        for (int i = 0; i < numPoints - 1; i++) {
            if (i == 0) {
                spline.interpolate(1f / numSegments, 0, tmp);
                node.setLocalTranslation(spline.getControlPoints().get(currentControlPoint));
                node.lookAt(tmp, Vector3f.UNIT_Y);
                
                node.localToWorld(new Vector3f(width * 0.5f, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;

                node.localToWorld(new Vector3f(-width * 0.5f, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;
                
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                
                texCoord[uvIndex++] = 0;
                texCoord[uvIndex++] = 0;
                texCoord[uvIndex++] = 1;
                texCoord[uvIndex++] = 0;
                
                texCoord2[uvIndex2++] = 0;
                texCoord2[uvIndex2++] = 0;
                texCoord2[uvIndex2++] = 1;
                texCoord2[uvIndex2++] = 0;
            } else {
                tmp = node.getLocalTranslation().clone();
                node.setLocalTranslation(spline.getControlPoints().get(currentControlPoint));
                node.lookAt(tmp, Vector3f.UNIT_Y);
                
                node.localToWorld(new Vector3f(-width, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;

                node.localToWorld(new Vector3f(width, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;
                
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                
                uvY += tmp.distance(node.getLocalTranslation()) * uvYSize;
                texCoord[uvIndex++] = 0;
                texCoord[uvIndex++] = uvY;
                texCoord[uvIndex++] = 1;
                texCoord[uvIndex++] = uvY;
            }
            
            for (int s = 1; s < numSegments; s++) {
                tmp = node.getLocalTranslation().clone();
                spline.interpolate((float)s / numSegments, currentControlPoint, tmp2);
                node.setLocalTranslation(tmp2);
                node.lookAt(tmp, Vector3f.UNIT_Y);
                float wPerc;
                if (currentControlPoint == 0) {
                    wPerc = (((float) s / numSegments) * 0.5f) + 0.5f;
                    texCoord2[uvIndex2++] = 0;
                    texCoord2[uvIndex2++] = (float) s / numSegments;
                    texCoord2[uvIndex2++] = 1;
                    texCoord2[uvIndex2++] = (float) s / numSegments;
                } else if ((currentControlPoint / 3) == numPoints - 2) {
                    wPerc = (0.5f - (((float) s / numSegments) * 0.5f)) + 0.5f;
                    texCoord2[uvIndex2++] = 0;
                    texCoord2[uvIndex2++] = 1f - ((float) s / numSegments);
                    texCoord2[uvIndex2++] = 1;
                    texCoord2[uvIndex2++] = 1f - ((float) s / numSegments);
                } else {
                    wPerc = 1f;
                    texCoord2[uvIndex2++] = 0;
                    texCoord2[uvIndex2++] = 1;
                    texCoord2[uvIndex2++] = 1;
                    texCoord2[uvIndex2++] = 1;
                }
                
                node.localToWorld(new Vector3f(-width * wPerc, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;
                
                node.localToWorld(new Vector3f(width * wPerc, 0f, 0f), tmp2);
                verts[index++] = tmp2.x;
                verts[index++] = 0f;
                verts[index++] = tmp2.z;
                
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                normals[nIndex++] = 0;
                normals[nIndex++] = 1;
                normals[nIndex++] = 0;
                
                uvY += tmp.distance(node.getLocalTranslation()) * uvYSize;
                texCoord[uvIndex++] = 0;
                texCoord[uvIndex++] = uvY;
                texCoord[uvIndex++] = 1;
                texCoord[uvIndex++] = uvY;
            }
            
            currentControlPoint += 3;
        }
        tmp = node.getLocalTranslation().clone();
        node.setLocalTranslation(spline.getControlPoints().get(spline.getControlPoints().size() - 1));
        node.lookAt(tmp, Vector3f.UNIT_Y);
        
        node.localToWorld(new Vector3f(-width * 0.5f, 0f, 0f), tmp2);
        verts[index++] = tmp2.x;
        verts[index++] = 0f;
        verts[index++] = tmp2.z;

        node.localToWorld(new Vector3f(width * 0.5f, 0f, 0f), tmp2);
        verts[index++] = tmp2.x;
        verts[index++] = 0f;
        verts[index++] = tmp2.z;
        node.removeFromParent();
        
        normals[nIndex++] = 0;
        normals[nIndex++] = 1;
        normals[nIndex++] = 0;
        normals[nIndex++] = 0;
        normals[nIndex++] = 1;
        normals[nIndex++] = 0;
        
        uvY += tmp.distance(node.getLocalTranslation()) * uvYSize;
        texCoord[uvIndex++] = 0;
        texCoord[uvIndex++] = uvY;
        texCoord[uvIndex++] = 1;
        texCoord[uvIndex++] = uvY;
        
        texCoord2[uvIndex2++] = 0;
        texCoord2[uvIndex2++] = 0;
        texCoord2[uvIndex2++] = 1;
        texCoord2[uvIndex2++] = 0;
        
        short[] indices = new short[((numPoints - 1) * numSegments) * 6];
        for (int ind = 0; ind < (numPoints - 1) * numSegments; ind++) {
            int indInd = ind * 6;
            int indV = ind * 2;
            indices[indInd++] = (short)indV;
            indices[indInd++] = (short)(indV + 1);
            indices[indInd++] = (short)(indV + 2);
            
            indices[indInd++] = (short)(indV + 1);
            indices[indInd++] = (short)(indV + 3);
            indices[indInd] = (short)(indV + 2);
        }
        
        setBuffer(VertexBuffer.Type.Position, 3, verts);
        setBuffer(VertexBuffer.Type.Normal, 3, normals);
        setBuffer(VertexBuffer.Type.TexCoord, 2, texCoord);
        setBuffer(VertexBuffer.Type.TexCoord2, 2, texCoord2);
        setBuffer(VertexBuffer.Type.Index, 3, indices);
        updateBound();
        updateCounts();
    }
}

P.S. The second set of UV coordinates on the cyclical bezier are used for an alpha mask using world coordinates in the shader. The alpha mask is the fog of war texture which is updated only on frames in which the state of the fog of war is changed.

When visibility of a particular area is changed a viewport is enabled, takes a single frame snapshot of a black and white scene rendered to an off-screen buffer which is then blurred over a few passes after which the viewport is disabled. The resolution of the texture depends on the size of the level, but it’s not very big. Blurring it adds a nice fade effect around the edges, but also prevents the texture from looking blocky when scaled up.

Hi @ all - I am realy new to JME. I try making something like an OpenWorld vehicle Playground…

2 Likes

Thank you very much for your explanation. I’m currently adding boundaries to my engine and was having a problem with interior hole (in my case, unpassable terrain such as water, mountains, under-fire…).

Currently , i’m using simple line mesh, with duplicated points (ie if a node as it north and north-west direction beeing a boundary, then the mesh will have 4 points (2 for north and 2 for north west). while this works (even for internal boundaries, since i’m adding the line when going from a passable to impassable node), this is not as nice as your spline-based one.

i really appreciate your sharing of code.
Thanks again

@hansdevries: That VW Bus looks great, reminds me of a good joke I once heard about VW busses: There’s something to be said about a vehicle that uses the driver’s legs as the first line of defense in an accident :smiley:

@mohira: Always happy to share and help out.

The thing about using splines to highlight these boundaries is the necessity to ensure the edges are detected in the order under which they exist in relation to one another. One cannot just look in one direction, find an edge, add a control point(s) then look in another direction, find another edge and add a control point because the controls need to be contiguous otherwise the curve is bouncing all over the place.

I imagine the way you’re doing it runs pretty quick. I think the Civilization series displayed boundaries in the same or similar manner. I decided to go the spline route because I wanted something more organic in appearance and I liked the challenge.

Basically what I did to get the proper order of nodes was to start out at the origin position, the node containing the unit or object of interest, and loop through the nodes above it one by one looking for the north edge. Technically I could’ve looked in any direction, I picked north. When I hit that edge I add the last node found within the unit’s range to a list of edge nodes.

I then check the nodes surrounding that edge node one by one counter clockwise starting with the north node looking for a node that is within the unit’s range, when I find it I add it to the list of edge nodes then check the surrounding nodes of the recently added node counter clockwise looking for the last node I checked. Once I find that I check the surrounding nodes of the most recently added node counter clockwise again starting with the node that comes after the previously added node in the counter clockwise series looking for the next node that is within the unit’s range and continue to do that until I reach back around to the beginning.

I like to imagine a slinky looping it’s way around the edges of the unit’s range.

Inner boundaries are done in a similar manner except I start at the bottom of the boundary rather than the top so I end up looping around the boundary in the opposite direction. I’m still checking surrounding nodes in a counter clockwise manner, but because I start at the bottom of the inner boundary I loop around the whole inner boundary clockwise rather than counter clockwise so the spline travels the opposite direction therefore the texture on the spline doesn’t need to be reversed.

Flower power.

2 Likes

Found a pretty easy way to make 2D collision shapes for the sprites I’m using… and I pretty trivially swapped out my nascent simple physics engine with dyn4j. It will save me tons of time.

4 Likes

Sorry @pspeed , can’t hide my amusement at seeing a core jme developer that shows a dozen 2d sprites struggling to reach 30FPS :smiley:

@tryder it’s currently speedy enough, but i’ll have to optimize some time in the future, i think.
i’m currently working on the map generator & unit loading from xml file… i hop to mimic the original game as far as i can…

look what i found in may mail (from galatic civilization III) :smile:

3 Likes

@mohira: Always been a big fan of Galactic Civilizations and Sins of a Solar Empire for that matter. Never played Full Metal Planet myself, first I’m hearing of it actually, but best wishes for your project I look forward to seeing more of it in the future.

the 30 fps is because of the VideoRecorderAppState.

The 30 FPS is because of FRAPS actually… of course. Un-vsynched and without fraps it runs at 2000 FPS or something.

1 Like

Might as well continue spamming here… :smile:

Video of arena flight:

Bonus, downloads of that version here:
http://www.simsilica.com/Downloads/EtherealSpace-test-20150413-Windows.zip
http://www.simsilica.com/Downloads/EtherealSpace-test-20150413-Linux.zip
http://www.simsilica.com/Downloads/EtherealSpace-test-20150413-MacOSX.zip

No networking yet… but soon… very soon.

Controls are: WASD

5 Likes

well ,it lookes good

Nice work. runs at 140 frames on

1440px x 900px display-resolution
HP Workstation from 2006
manjaro linux