Search code examples
java3djmonkeyengineuv-mapping

How to locate texture on sphere in jME3?


I would like to place JPEG texture map on sphere. It works for me, but I want to rotate texture by 180 degrees. I.e I want image to start not from zero UV coordinates, but earlier.

enter image description here

UPDATE

I have tried to reassign texture coordinates of a sphere. Texture coordinates are float, and I was hoping they are not constrained to the range of [0..1]. Otherwise it should placed my image into the region of [0..1 x 0..1].

It did something like latter, but not precise:

enter image description here

I.e. entire image was put into small region of a sphere. But, this exact region, where it is located, corresponds with negative values of U, i.e. at the same longitude, where image margin was in previous experiment (top sphere).

Why?

Image is here: https://en.wikipedia.org/wiki/File:Equirectangular_projection_SW.jpg

The code is follows:

package tests.com.jme3;

import java.nio.FloatBuffer;

import com.jme3.app.SimpleApplication;
import com.jme3.font.BitmapText;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.shape.Sphere;
import com.jme3.util.BufferUtils;

public class Try_TextureTransform  extends SimpleApplication {

    public static void main(String[] args) {
        Try_TextureTransform app = new Try_TextureTransform();
        app.setShowSettings(false);
        app.start(); // start the game
    }

    final float speed = 0.01f;

    BitmapText hudText;
    Sphere sphere1Mesh, sphere2Mesh;
    Material sphere1Mat, sphere2Mat;
    Geometry sphere1Geo, sphere2Geo;
    Quaternion orientation;
    DirectionalLight sun;

    @Override
    public void simpleInitApp() {

        flyCam.setEnabled(false);
        setDisplayStatView(false); 
        setDisplayFps(false);


        hudText = new BitmapText(guiFont, false);          
        hudText.setSize(guiFont.getCharSet().getRenderedSize());      // font size
        hudText.setColor(ColorRGBA.Blue);                             // font color
        hudText.setText("");             // the text
        hudText.setLocalTranslation(300, hudText.getLineHeight()*2, 0); // position
        guiNode.attachChild(hudText);

        sphere1Mesh = new Sphere(50, 50, 2);
        sphere1Mesh.setTextureMode(Sphere.TextureMode.Projected); // matrc

        sphere1Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        sphere1Mat.setTexture("ColorMap", assetManager.loadTexture("textures/Equirectangular_projection_SW.jpg"));

        sphere1Geo = new Geometry("Sphere2", sphere1Mesh);
        sphere1Geo.setMaterial(sphere1Mat); 
        sphere1Geo.setLocalTranslation(0, 0, 2);

        sphere2Mesh = new Sphere(50, 50, 2);

        VertexBuffer vb = sphere2Mesh.getBuffer(Type.Position);
        FloatBuffer fb = (FloatBuffer) vb.getData();
        float[] vertexCoordinates = BufferUtils.getFloatArray(fb);

        VertexBuffer vb2 = sphere2Mesh.getBuffer(Type.TexCoord);
        FloatBuffer fb2 = (FloatBuffer) vb2.getData();
        float[] uvCoordinates = BufferUtils.getFloatArray(fb2);

        double rho;
        for (int i = 0; i < vertexCoordinates.length/3; ++i) {

            uvCoordinates[i*2] = (float) Math.atan2(vertexCoordinates[i*3+1], vertexCoordinates[i*3]);
            rho = Math.sqrt(Math.pow( vertexCoordinates[i*3], 2) + Math.pow( vertexCoordinates[i*3+1], 2));
            uvCoordinates[i*2+1] = (float) Math.atan2(vertexCoordinates[i*3+2], rho);
        }
      //apply new texture coordinates
        VertexBuffer uvCoordsBuffer = new VertexBuffer(Type.TexCoord);
        uvCoordsBuffer.setupData(Usage.Static, 2, com.jme3.scene.VertexBuffer.Format.Float, BufferUtils.createFloatBuffer(uvCoordinates));
        sphere2Mesh.clearBuffer(Type.TexCoord);
        sphere2Mesh.setBuffer(uvCoordsBuffer);


        //sphere2Mesh.setTextureMode(Sphere.TextureMode.Projected); // better quality on spheres

        sphere2Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        sphere2Mat.setTexture("ColorMap", assetManager.loadTexture("textures/Equirectangular_projection_SW.jpg"));

        sphere2Geo = new Geometry("Sphere2", sphere2Mesh);
        sphere2Geo.setMaterial(sphere2Mat); 
        sphere2Geo.setLocalTranslation(0, 0, -2);

        cam.setLocation(new Vector3f(-10, 0, 0));
        cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Z);

        rootNode.attachChild(sphere1Geo);
        rootNode.attachChild(sphere2Geo); 

    }

    @Override
    public void simpleUpdate(float tpf) {


        Vector2f cursorPosition = inputManager.getCursorPosition();
        Vector3f cursorPositionWorld = cam.getWorldCoordinates(cursorPosition, 1);

        orientation = new Quaternion().fromAngleAxis(cursorPositionWorld.z*speed, Vector3f.UNIT_Y);
        orientation.multLocal(new Quaternion().fromAngleAxis(-cursorPositionWorld.y*speed, Vector3f.UNIT_Z));

        rootNode.setLocalRotation(orientation);



    }

}

Solution

  • The correct way to do this is just to rotate the geometry as you see fit or edit the texture (techniques 1 and 2) but because you talk about modifying the texture coordinates themselves I include techniques 3 and 4 in case you are using this example to learn a larger technique for when it is appropriate.

    Technique 1 - Rotate the geometry

    Rotate the geometry so that it is orientated the way you want it. This is by far the easiest, most appropriate and most understandable technique and what I recommend

        //Add this
        Quaternion quat=new Quaternion();
        quat.fromAngles(0 ,0 , FastMath.PI);
        sphere1Geo.setLocalRotation(quat);
    

    enter image description here

    Complete program

    public class Main extends SimpleApplication {
    
        public static void main(String[] args) {
            Main app = new Main();
            app.setShowSettings(false);
            app.start(); // start the game
        }
    
        final float speed = 0.01f;
    
        BitmapText hudText;
        Quaternion orientation;
        DirectionalLight sun;
    
        @Override
        public void simpleInitApp() {
    
            flyCam.setEnabled(false);
            setDisplayStatView(false); 
            setDisplayFps(false);
    
    
            hudText = new BitmapText(guiFont, false);          
            hudText.setSize(guiFont.getCharSet().getRenderedSize());      // font size
            hudText.setColor(ColorRGBA.Blue);                             // font color
            hudText.setText("");             // the text
            hudText.setLocalTranslation(300, hudText.getLineHeight()*2, 0); // position
            guiNode.attachChild(hudText);
    
            cam.setLocation(new Vector3f(10, 0, 0));
            cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Z);
    
            addOriginalSphere();
            addRotatedSphere();
    
        }
    
        public void addOriginalSphere(){
            Sphere sphere1Mesh = new Sphere(50, 50, 2);
            sphere1Mesh.setTextureMode(Sphere.TextureMode.Projected); // matrc
    
            Material sphere1Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
            sphere1Mat.setTexture("ColorMap", assetManager.loadTexture("Textures/world.png"));
    
            Geometry sphere1Geo = new Geometry("Original Sphere", sphere1Mesh);
            sphere1Geo.setMaterial(sphere1Mat); 
            sphere1Geo.setLocalTranslation(0, -2, 0);
    
            rootNode.attachChild(sphere1Geo);
        }
        public void addRotatedSphere(){
            Sphere sphere1Mesh = new Sphere(50, 50, 2);
            sphere1Mesh.setTextureMode(Sphere.TextureMode.Projected); // matrc
    
            Material sphere1Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
            sphere1Mat.setTexture("ColorMap", assetManager.loadTexture("Textures/world.png"));
    
            Geometry sphere1Geo = new Geometry("Rotated Sphere", sphere1Mesh);
            sphere1Geo.setMaterial(sphere1Mat); 
            sphere1Geo.setLocalTranslation(0, 2, 0);
    
            //Add this
            Quaternion quat=new Quaternion();
            quat.fromAngles(0 ,0 , FastMath.PI);
            sphere1Geo.setLocalRotation(quat);
    
            rootNode.attachChild(sphere1Geo);
        }
    
        @Override
        public void simpleUpdate(float tpf) {
    
    
    
        }
    
    }
    

    Technique 2 - Edit the texture to conform to the way you want it to be

    Many image editing programs exist, the one I use is Paint.Net and (like most editing software) gives exact pixel mouse coordinates. Just cut and paste the image such that greenwich is at the far left. In your case you need to edit the image anyway because it has that horrible white border on it.

    Technique 3 - Mess with the vertex texture co-ordinates

    This is overkill for this and is not what I recomend. But if this is an excercise to learn to create your own custom mesh then read on

    public void addRotatedSphere_ByMessingWithMesh(){
        Sphere sphere1Mesh = new Sphere(50, 50, 2);
        sphere1Mesh.setTextureMode(Sphere.TextureMode.Projected); // matrc
    
    
        FloatBuffer textureBuffer=sphere1Mesh.getFloatBuffer(Type.TexCoord);
    
        float[] newTextureCoordinates=new float[textureBuffer.capacity()];
    
    
        for(int i=0;i<newTextureCoordinates.length;i++){
            //texture buffer goes x co-ordinate, y coordinate, x coordinate, y coordinate
            if (i%2!=1){
                newTextureCoordinates[i]=(float)((textureBuffer.get(i)+0.5)%1);
            }else{
                newTextureCoordinates[i]=textureBuffer.get(i);
            }
        }
    
        sphere1Mesh.setBuffer(Type.TexCoord, 2,newTextureCoordinates);
    
        Material sphere1Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        sphere1Mat.setTexture("ColorMap", assetManager.loadTexture("Textures/world.png"));
    
    
    
        Geometry sphere1Geo = new Geometry("Rotated Sphere", sphere1Mesh);
        sphere1Geo.setMaterial(sphere1Mat); 
        sphere1Geo.setLocalTranslation(0, 2, 0);
    
    
    
        rootNode.attachChild(sphere1Geo);
    }
    

    enter image description here

    This has a problem because the seam at the back is not done properly; because the true texture coordinates go 0,0.2,0.4,0.8,1. Whereas out new ones do a wrap around on the far side. In this specific example you can do a manual handling of the seam but you can already see that this is a pain.

    Technique 4 - Write your own shader

    This is bordering on rediculus but you could write a custom shader that would take the true texture coordinates and apply a transformation similar to the one performed within Technique 3, but this would be done on the graphics card and is a nightmare to debug.

    It goes without saying that that would be using a small nuclear weapon to kill a fly and I shall not explain all the step explicity (but its heavily based on unshaded.j3md and unshaded.vert

    • Create the following files to define our new material

    enter image description here

    Material definition

    Only change is to mention our custom vertex shader rather than use the custom one

    MaterialDef Unshaded {
    
        MaterialParameters {
            Texture2D ColorMap
            Texture2D LightMap
            Color Color (Color)
            Boolean VertexColor (UseVertexColor)
            Boolean SeparateTexCoord
    
            // Texture of the glowing parts of the material
            Texture2D GlowMap
            // The glow color of the object
            Color GlowColor
    
            // For hardware skinning
            Int NumberOfBones
            Matrix4Array BoneMatrices
    
            // Alpha threshold for fragment discarding
            Float AlphaDiscardThreshold (AlphaTestFallOff)
    
            //Shadows
            Int FilterMode
            Boolean HardwareShadows
    
            Texture2D ShadowMap0
            Texture2D ShadowMap1
            Texture2D ShadowMap2
            Texture2D ShadowMap3
            //pointLights
            Texture2D ShadowMap4
            Texture2D ShadowMap5
    
            Float ShadowIntensity
            Vector4 Splits
            Vector2 FadeInfo
    
            Matrix4 LightViewProjectionMatrix0
            Matrix4 LightViewProjectionMatrix1
            Matrix4 LightViewProjectionMatrix2
            Matrix4 LightViewProjectionMatrix3
            //pointLight
            Matrix4 LightViewProjectionMatrix4
            Matrix4 LightViewProjectionMatrix5
            Vector3 LightPos
            Vector3 LightDir
    
            Float PCFEdge
    
            Float ShadowMapSize
        }
    
        Technique {
            VertexShader GLSL100:   MatDefs/TextureSplitting.vert
            FragmentShader GLSL100: Common/MatDefs/Misc/Unshaded.frag
    
            WorldParameters {
                WorldViewProjectionMatrix
            }
    
            Defines {
                SEPARATE_TEXCOORD : SeparateTexCoord
                HAS_COLORMAP : ColorMap
                HAS_LIGHTMAP : LightMap
                HAS_VERTEXCOLOR : VertexColor
                HAS_COLOR : Color
                NUM_BONES : NumberOfBones
                DISCARD_ALPHA : AlphaDiscardThreshold
            }
        }
    
        Technique {
        }
    
        Technique PreNormalPass {
    
              VertexShader GLSL100 :   Common/MatDefs/SSAO/normal.vert
              FragmentShader GLSL100 : Common/MatDefs/SSAO/normal.frag
    
              WorldParameters {
                  WorldViewProjectionMatrix
                  WorldViewMatrix
                  NormalMatrix
              }
    
              Defines {
                  NUM_BONES : NumberOfBones
              }
       }
    
        Technique PreShadow {
    
            VertexShader GLSL100 :   Common/MatDefs/Shadow/PreShadow.vert
            FragmentShader GLSL100 : Common/MatDefs/Shadow/PreShadow.frag
    
            WorldParameters {
                WorldViewProjectionMatrix
                WorldViewMatrix
            }
    
            Defines {
                COLOR_MAP : ColorMap
                DISCARD_ALPHA : AlphaDiscardThreshold
                NUM_BONES : NumberOfBones
            }
    
            ForcedRenderState {
                FaceCull Off
                DepthTest On
                DepthWrite On
                PolyOffset 5 3
                ColorWrite Off
            }
    
        }
    
    
        Technique PostShadow15{
            VertexShader GLSL150:   Common/MatDefs/Shadow/PostShadow15.vert
            FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadow15.frag
    
            WorldParameters {
                WorldViewProjectionMatrix
                WorldMatrix
            }
    
            Defines {
                HARDWARE_SHADOWS : HardwareShadows
                FILTER_MODE : FilterMode
                PCFEDGE : PCFEdge
                DISCARD_ALPHA : AlphaDiscardThreshold           
                COLOR_MAP : ColorMap
                SHADOWMAP_SIZE : ShadowMapSize
                FADE : FadeInfo
                PSSM : Splits
                POINTLIGHT : LightViewProjectionMatrix5
                NUM_BONES : NumberOfBones
            }
    
            ForcedRenderState {
                Blend Modulate
                DepthWrite Off                 
                PolyOffset -0.1 0
            }
        }
    
        Technique PostShadow{
            VertexShader GLSL100:   Common/MatDefs/Shadow/PostShadow.vert
            FragmentShader GLSL100: Common/MatDefs/Shadow/PostShadow.frag
    
            WorldParameters {
                WorldViewProjectionMatrix
                WorldMatrix
            }
    
            Defines {
                HARDWARE_SHADOWS : HardwareShadows
                FILTER_MODE : FilterMode
                PCFEDGE : PCFEdge
                DISCARD_ALPHA : AlphaDiscardThreshold           
                COLOR_MAP : ColorMap
                SHADOWMAP_SIZE : ShadowMapSize
                FADE : FadeInfo
                PSSM : Splits
                POINTLIGHT : LightViewProjectionMatrix5
                NUM_BONES : NumberOfBones
            }
    
            ForcedRenderState {
                Blend Modulate
                DepthWrite Off   
                PolyOffset -0.1 0  
            }
        }
    
        Technique Glow {
    
            VertexShader GLSL100:   Common/MatDefs/Misc/TextureSplitting.vert
            FragmentShader GLSL100: Common/MatDefs/Light/Glow.frag
    
            WorldParameters {
                WorldViewProjectionMatrix
            }
    
            Defines {
                NEED_TEXCOORD1
                HAS_GLOWMAP : GlowMap
                HAS_GLOWCOLOR : GlowColor
                NUM_BONES : NumberOfBones
            }
        }
    }
    

    Vertex shader

    Use a translation to map the true texture coordinates to the shifted coordinates. Incidently if you think this isn't java; it isn't. Its OpenGL Shader Langauge.

    #import "Common/ShaderLib/Skinning.glsllib"
    
    uniform mat4 g_WorldViewProjectionMatrix;
    attribute vec3 inPosition;
    
    #if defined(HAS_COLORMAP) || (defined(HAS_LIGHTMAP) && !defined(SEPARATE_TEXCOORD))
        #define NEED_TEXCOORD1
    #endif
    
    attribute vec2 inTexCoord;
    attribute vec2 inTexCoord2;
    attribute vec4 inColor;
    
    varying vec2 texCoord1;
    varying vec2 texCoord2;
    
    varying vec4 vertColor;
    
    void main(){
        #ifdef NEED_TEXCOORD1
            texCoord1 = inTexCoord;
            texCoord1.x=texCoord1.x+0.5;
            if (texCoord1.x>1){
                texCoord1.x=texCoord1.x-1;
            }
        #endif
    
        #ifdef SEPARATE_TEXCOORD
            texCoord2 = inTexCoord2;
        #endif
    
        #ifdef HAS_VERTEXCOLOR
            vertColor = inColor;
        #endif
    
        vec4 modelSpacePos = vec4(inPosition, 1.0);
        #ifdef NUM_BONES
            Skinning_Compute(modelSpacePos);
        #endif
        gl_Position = g_WorldViewProjectionMatrix * modelSpacePos;
    }
    

    Then use this as a material instead of unshaded.j3md

    Material sphere1Mat = new Material(assetManager, "Materials/TextureSplitting.j3md");
    

    enter image description here

    Again there is a nasty break around the back where the true texture roles over between 0 and 1 which we could handle explicitly if we wanted but we'd have to make sure there were 2 vertexs at the split point one with texture coordinate 0 and one with texture coordinate 1.

    Conclusion

    Techniques 1 or 2 are the ones you should use. I include techniques 3 and 4 simply to show that you can do this using the actual texture coordinates but that you shouldn't.