DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How To Check for JSON Insecure Deserialization (JID) Attacks With Java
  • Refactoring Java Application: Object-Oriented And Functional Approaches
  • JVM Memory Architecture and GC Algorithm Basics
  • High-Performance Java Serialization to Different Formats

Trending

  • Evolution of Cloud Services for MCP/A2A Protocols in AI Agents
  • Event-Driven Architectures: Designing Scalable and Resilient Cloud Solutions
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Rethinking Recruitment: A Journey Through Hiring Practices
  1. DZone
  2. Coding
  3. Java
  4. 3D Model Interaction with Java 3D

3D Model Interaction with Java 3D

By 
Dalton Filho user avatar
Dalton Filho
·
Jan. 26, 08 · News
Likes (0)
Comment
Save
Tweet
Share
64.4K Views

Join the DZone community and get the full member experience.

Join For Free

This tutorial is based on a computer graphics assignment for which i was given the task of creating an application in which some articulated animal would walk using a hierarchical model. I had 4 days to complete this assignment, so i had to learn java 3d quickly, but i ended up having to read fragmented documentation, mostly focused on theory i already knew, with practical examples that were either too simple or too complex.


The objective of this tutorial is to provide a guide for writing a basic java 3d application with a 3d model loaded from disk; it's less generic than the official java3d application tutorial and less focused on theory than other tutorials, but more straightforward for the experienced java developer who already knows basic cg theory and just wants to know what goes where very quickly - going deeper in the APIs is up to you.

Requirements


  •  JDK  version 1.5 or above (the examples use java 5 features)
  •  java 3d  version 1.4 or above installed
  • experience with jfc
  • basic computer graphics knowledge (3d transforms, illumination types)
  • a 3d model visualizer, like  poseray 
  • a 3d model converter like  3dwin  will be useful if you find some interesting 3d model in a format not supported by any java 3d loader

Models

You can download free 3d models on websites like turbosquid  or  the 3d archive . Free models may not have the quality you are looking for, so if you are on a serious/commercial project, you should probably consider purchasing a quality model. If you really want to model your objects you can try  blender.

Visualizing the model

I will use a  cockroach I downloaded from the 3d archive. You can choose another model if you will as long you know what you're doing. I will use poseray to visualize the model. Poseray cannot open every 3d format, so if the format of your model is not supported by poseray, you will have to use some other program like 3dwin to convert it to a format poseray accepts. poseray is actually intended to work with moray and povray, but it works very well for the purpose of viewing 3d models.

 1. load the model   2. check the model (shot #1) 
 3. check the model (shot #2)   4. check how this model is branched 

These are the pieces that form the complete model. you will have to analyze how your model is branched to see if you can animate or interact with it as you plan. Every component of the model has a name - let it be the parts of your main subject or just other components from the scene. You can get these names on your program, but it's easier to check which part is which here. you can use the update function if the names aren't descriptive enough. You will need to know the name of every part if you plan to texturize or animate them independently.

In the end, all that matters is that you save your file in a format that java loaders will recognize. preferably, save it in the wavefront .obj or lightwave . LWO format because java 3d comes with loaders for these file formats by default. Other loaders are available, but you will have to download them separately.  Other java3d loaders 

Loading the model

Wavefront .obj format

            import java.io.filereader;
            import java.io.ioexception;
            import com.sun.j3d.loaders.scene; // contains the object loaded from disk.
            import com.sun.j3d.loaders.objectfile.objectfile; // loader of .obj models

            public static scene loadscene(string location) throws ioexception {
                    objectfile loader = new objectfile(objectfile.resize); 
                    return loader.load(new filereader(location)); 
            }
        

Lightwave .lwo format

            import com.sun.j3d.loaders.lw3d.lw3dloader; // loader of .lwo models

            public static scene loadscene(string location) throws ioexception {
                    lw3dloader loader = new lw3dloader(); 
                    return loader.load(new filereader(location)); 
            }
        

Recommended reading:  objectfile javadoc  ,  lw3dloader javadoc  .

Basic setup

Now that you know how to load the model let's see how it will look on your program before proceeding to further manipulation. the most important class of this example is the simple universe, which saves you from having to configure the view of your scene. a directional light is added to allow you to view your object (no light and you will see a plain black). You can view the source in .

spooky


 Roach as seen on the example program 

Recommended reading:  simpleuniverse javadoc 

Getting the scene components

We need to obtain a reference to every body part we need to manipulate (or just scene component, if you are not using a model of an animal). If you want to create a variable for every component and assign a meaningful name to each one, you will have to know what name maps to what component. the following piece of code demonstrates how to list the name of every named object from the scene:

            import javax.media.j3d.shape3d;

            void listscenenamedobjects(scene scene) {
                map<string, shape3d> namemap = scene.getnamedobjects(); 

                for (string name : namemap.keyset()) {
                    system.out.printf("name: %s\n", name); 
                }
            }
        

Have in mind that every  shape3d  is already part of the  branchgroup  of the scene, you have loaded. If you want to create another graph with your custom hierarchy, you will have to get a reference to one specific  shape3d  and then remove it from the  branchgroup  :

            import javax.media.j3d.branchgroup;

            /* obtains a reference to a specific component in the scene */
            shape3d eyes = namemap.get("eyes"); 

            /* the graph that still contains a reference to "eyes" */
            branchgroup root = scene.getscenegroup();

            /* removes "eyes" from this graph */
            root.removechild(eyes);

            /* now you are free to use "eyes" in your custom graph */ 
        

Always remember you cannot add a component to more than one graph. If one component is already part of a graph and you try to add it to another, you will get a  multipleparentexception.  If you need the same component in more than one graph, you can clone them.

Transformations

 Basic transformation steps: 

  1. Add the parts you want to transform to a  transformgroup  ;
  2. Apply the  transformgroup.allow_transform_write  capability to the group if it wasn't set;
  3. Create or use some previously created instance of  transform3d  ;
  4. Configure this instance of  transform3d  as / if necessary;
  5. Apply this  transform3d  instance on the  transformgroup  instance.

That implies you will have to keep references to instances of these classes in order to transform specific nodes of your graph.

The following piece of code demonstrates translation, rotation on multiple axis and non-uniform scaling. It uses code created on previous sections.

            import javax.vecmath.vector3f;
            import javax.vecmath.vector3d;
            import javax.media.j3d.transformgroup;
            import javax.media.j3d.transform3d; 


            map<string, shape3d> namemap = scene.getnamedobjects();

            /* get the node you want to transform */
            shape3d wing = namemap.get("wing");

            /* add it to a transformgroup */
            transformgroup transformgroup = new transformgroup();
            transformgroup.addchild(wing);

            /* necessary to allow this group to be transformed */
            transformgroup.setcapability(transformgroup.allow_transform_write);

            /* accumulates all transforms */
            transform3d transforms = new transform3d();

            /* creates rotation transforms for x, y and z axis */
            transform3d rotx = new transform3d();
            transform3d roty = new transform3d();
            transform3d rotz = new transform3d();

            rotx.rotx(15d); // +15 degrees on the x axis
            roty.roty(30d); // +30 degrees on the y axis
            rotz.rotz(-20d); // -20 degrees on the z axis

            /* combines all rotation transforms */
            transforms.mul(rotx, roty);
            transforms.mul(transforms, rotz);

            /* translation: translates 2 on x, 3 on y and -10 on z */
            vector3f translationvector = new vector3f(2f, 3f, -10f);
            transforms.settranslation(translationvector);

            /* non uniform scaling: scales 3x on x, 1x on y and 2x on z */
            vector3d scale = new vector3d(3d, 1d, 2d);
            transforms.setscale(scale);

            /* apply all transformations */ 
            transformgroup.settransform(transforms);
        

Recommended reading:  transform3d javadoc  ,  transformgroup javadoc 

Hierarchical model

Now that you have access to all components separately, you can build your custom hierarchical graph.

If you have been using swing or awt, you are already familiar with the hierarchical model. for instance, you can have a  jframe  , which then adds a  jpanel  , which then adds a  jlabel  and so forth. Many properties applied on the root are propagated to children, like the  isvisible()  property. With a 3d model, all transforms and texturizations will be applied to all children (subgraphs). imagine if you had to apply the same transform over and over to many model parts just to make one movement?

Java 3d has a class called  group  , which is basically an n-tree: every children has only one parent and an arbitrary number of children. you will use subclasses of  group  to create your scenes. java 3d has also the  leaf  class, which is used to construct objects on the tree which wouldn't make sense with children, like background, camera, behaviour, etc.

scene graph

 hierarchical model of the scene 

I will use the  transformgroup  class as the default node for building the graph. you may use other subclasses of  group  if you have other needs. You may want to keep a reference of every  transformgroup  you create if you are going to do some interaction (like making a cockroach walk).

Note that the code above suffers from the same flaws of programatic gui construction. you can define the graph in xml and create a custom parser if you need reusability. if possible, you can also edit the model graph in a model editor to avoid having to perform these steps on your program.

Hierarchical construction of the graph 

            transformgroup getcockroach(scene scene) {

                /* obtain the scene's branchgroup, from which components are removed */
                branchgroup root = scene.getscenegroup();

                map<string, shape3d> namemap = scene.getnamedobjects();

                /* remove all children (you don't want a multiparentexception) */
                root.removeallchildren();

                /* construct the groups */
                transformgroup leftlegs = new transformgroup();
                transformgroup rightlegs = new transformgroup();
                transformgroup body = new transformgroup();
                transformgroup roach = new transformgroup();

                /* build the graph --> left legs */
                leftlegs.addchild(namemap.get("luplegf"));
                leftlegs.addchild(namemap.get("luplegm"));
                leftlegs.addchild(namemap.get("luplegr"));
                leftlegs.addchild(namemap.get("lmidlegf"));
                leftlegs.addchild(namemap.get("lmidlegm"));
                leftlegs.addchild(namemap.get("lmidlegr"));
                leftlegs.addchild(namemap.get("llowlegf"));
                leftlegs.addchild(namemap.get("llowlegm"));
                leftlegs.addchild(namemap.get("llowlegr"));
                leftlegs.addchild(namemap.get("lfootf"));
                leftlegs.addchild(namemap.get("lfootm"));
                leftlegs.addchild(namemap.get("lfootr"));

                /* build the graph --> right legs */
                rightlegs.addchild(namemap.get("ruplegf"));
                rightlegs.addchild(namemap.get("ruplegm"));
                rightlegs.addchild(namemap.get("ruplegr"));
                rightlegs.addchild(namemap.get("rmidlegf"));
                rightlegs.addchild(namemap.get("rmidlegm"));
                rightlegs.addchild(namemap.get("rmidlegr"));
                rightlegs.addchild(namemap.get("rlowlegf"));
                rightlegs.addchild(namemap.get("rlowlegm"));
                rightlegs.addchild(namemap.get("rlowlegr"));
                rightlegs.addchild(namemap.get("rfootf"));
                rightlegs.addchild(namemap.get("rfootm"));
                rightlegs.addchild(namemap.get("rfootr"));

                /* build the graph --> remaining body */
                body.addchild(namemap.get("antena"));
                body.addchild(namemap.get("antenar"));
                body.addchild(namemap.get("wing"));
                body.addchild(namemap.get("abdomen"));
                body.addchild(namemap.get("head"));
                body.addchild(namemap.get("prothorx"));
                body.addchild(namemap.get("eyes"));
                body.addchild(namemap.get("lpalp"));
                body.addchild(namemap.get("rpalp"));

                /* build the graph --> roach */
                roach.addchild(leftlegs);
                roach.addchild(rightlegs);
                roach.addchild(body);

                /* enable transform capability  (it is not enabled by default) */
                enabletransformcapability(leftlegs, rightlegs, body, roach);

                return roach;
            }

            void enabletransformcapability(transformgroup... parts) {
                for (transformgroup part : parts) {
                    part.setcapability(transformgroup.allow_transform_write);
                }
            }
        

Note that i have declared the transform groups locally, but on your program you will have to declare them globally or keep a reference to them somewhere if you plan to add interaction to your model. we will configure the camera (actually a view) and add lights .

I did a fairly simple hierarchy because the movement this cockroach will do is just as simple. in my assignment i had to do an interaction in which the legs would articulate, which implied in a different (i.e. more complex) setup for the hierarchy of the legs.

Appearance

The loaded cockroach is quite pale since no material descriptors were associated with it, but this is not a problem, as you can define your textures for each component of your graph. you must read the  material javadoc  to understand what is being done here.

To save some effort, I will declare some constants for ambient, emissive and specular light colors. the user may choose the diffuse color - the light which is emitted when the object is under the influence of some light.

            import javax.vecmath.color3f; 

            private static final color3f specular_light_color = new color3f(color.white);
            private static final color3f ambient_light_color = new color3f(color.light_gray);
            private static final color3f emissive_light_color = new color3f(color.black);
        

Now you can create a method that returns an  apperance  based on a given  color  :

            import javax.media.j3d.material;
            import javax.media.j3d.appearance;

            appearance getappearance(color color) {
                appearance app = new appearance();
                app.setmaterial(getmaterial(color));
                return app;
            }

            material getmaterial(color color) {
                return new material(ambient_light_color, 
                                    emissive_light_color, 
                                    new color3f(color), 
                                    specular_light_color, 
                                    100f);
            }
        

It's possible to use an image as a texture, but there are some constraints: the image must be equal in width and height and must be a power of 2. If you have ever used swing, you know you have to pass an instance of  component  to  mediatracker  if you want to track the loading of an image. loading a texture uses a similar process:

            import javax.media.j3d.texture2d;
            import com.sun.j3d.utils.image.textureloader;

            appearance getappearance(string path, component canvas, int dimension) {
                appearance appearance = new appearance();
                appearance.settexture(gettexture(path, canvas, dimension));
                return appearance;
            }

            texture gettexture(string path, component canvas, int dimension) {
                textureloader textureloader = new textureloader(path, canvas);

                texture2d texture = new texture2d(texture2d.base_level, 
                                                  texture2d.rgb, 
                                                  dimension, 
                                                  dimension); 

                texture.setimage(0, textureloader.getimage());

                return texture;
            }           
        

Applying the material:

            scene cockroach = getscenefromfile("roach_mod.obj");
            map<string, shape3d> namemap = cockroach.getnamedobjects();

            color brown = new color(165, 42, 42);
            appearance brownappearance = getappearance(brown);

            namemap.get("wing").setappearance(brownappearance);
        
 a material responds to different light positions 

As far as I've tested, if you assign a texture instead of a material, the object will not respond to different light configurations, instead, it will look like being constantly illuminated.

properly textured roach

 roach with a texture 

Lights

As you have seen, we still need to add two lights and one camera (a view). if you have read the , you have seen a directional light being added to the root of the scene. it's interesting to make the light go with the roach wherever it goes if you don't want it to get completely black after walking out of the reach of the light - in this case you will need to add your lights as leafs on the same node which contains the object you want to illuminate. On the other hand, if you want your object to become shadowed as it moves, you should add the lights to a node other than the one you used to add the model.

Except from finding the right vector to point the light to your object, creating and configuring lights is mostly simple. the following figure demonstrates how to construct an ambient light and a directional light:

            import javax.media.j3d.directionallight;
            import javax.media.j3d.ambientlight;


            color3f directionallightcolor = new color3f(color.blue);
            color3f ambientlightcolor = new color3f(color.white);
            vector3f lightdirection = new vector3f(-1f, -1f, -1f);

            ambientlight ambientlight = new ambientlight(ambientlightcolor);
            directionallight directionallight = new directionallight(directionallightcolor, lightdirection);

            bounds influenceregion = new boundingsphere();

            ambientlight.setinfluencingbounds(influenceregion);
            directionallight.setinfluencingbounds(influenceregion);            
        

Why do you need an influence region? for the same reason you need clipping: to avoid doing useless calculations. see the  light javadoc  for more information.

camera

If you want to view your scene on different angles, you will need a camera. java 3d uses a view based model - there are no camera objects, but a  viewplatform  object. Whenever you want to change the view of your scene, all you have to do is to change parameters on the  viewplatform  object. If you are using  simpleuniverse  to facilitate the view configuration of your program, you don't need to add any  viewplatform  instance to the root node because  simpleuniverse  has already added that for you.

the  viewplatform  created by  simpleuniverse  is inside a  multitransformgroup  , which you can obtain via  view  ing  platform  . The following code demonstrates how to obtain this  multitransformgroup  and use it to change the view of the scene:

            import com.sun.j3d.utils.universe.viewingplatform;


            /* you don't have to create a viewingplatform if you are using simpleuniverse */
            viewingplatform vp = universe.getviewingplatform();

            /* you don't need to add the vp to a transformgroup because the vp is already added in a multitransformgroup; 
               0 is the topmost transformgroup */
            transformgroup vpgroup = vp.getmultitransformgroup().gettransformgroup(0);

            /* you can transform the view platform as you do with other objects  */
            transform3d vptranslation = new transform3d(); 
            vector3f translationvector = new vector3f(1.9f, 1.2f, 6f);
            
            vptranslation.settranslation(translationvector);
            vpgroup.settransform(vptranslation);
        
 example: translation vectors used on the viewplatorm 
 0.0, -1.2, 6.0 
 1.9, 1.2, 6.0 
 0.0, 1.2, 6.0 
 -1.9, 1.2, 6.0 

Do not confuse  viewplatform  with  view  ing  platform  - the latter is a convenience class used to "set up the view side of the graph" - it  contains  a  viewplatform  .

Recommended reading:  viewingplatform javadoc  ,  viewplatform javadoc 

Background

Unless you want your background to be plain black, you should specify one. just remember to always add the background to the root node of your scene; add it anywhere else and you will get an undesirable  illegalsharingexception  .

color background

            import javax.media.j3d.background;


            /* a dull gray background */
            background background = new background(new color3f(color.light_gray)); 

            /* incluencregion is a boundingsphere. see the "lights" section for details */
            background.setapplicationbounds(influenceregion); 

            /* root is a branchgroup, root node of your scene object */
            root.addchild(background); 
        

Image background

            textureloader t = new textureloader("leaves.jpg", canvas);

            background background = new background(t.getimage()); 
            background.setimagescalemode(background.scale_repeat); // tiles the image
            background.setapplicationbounds(influenceregion); 

            root.addchild(background);
        

This static background is quite boring. If you are looking for something more interesting, such as a celestial sphere, you should use apply a geometry to a background. you can find examples on  java2s  website.

Recommended reading:  background javadoc 

Interacting with the model

Now it's time to use the  transformgroup  references you've kept a while ago. You will use them to control the movement of the model. The cockroach will do a very silly movement: the left legs will move forward while the right legs stand still, then the right legs move forward while left legs stand still; the body will always move a little bit forward on every movement. it's far from realistic, but you can derive more complex movements if you learn this one. (if you're concerned, as far as my assignment, the movement was more complex than that...)

the class  behavior  will be used to interact with the model. The  behavior class is like a listener - you have to implement it to achieve the desired reaction. It has to be activated every time it's used, or it won't react to the next  stimulus  . The  stimulus  used on this section will be a key press, but you can use many others - check   wakeupcriterion   's direct known subclasses to check for other options. After implementing your  behavior  subclass, all you have to do is to add it on the node you want to animate. 

Instance variables

            /** groups that will be animated. */
            transformgroup[] groups;

            /** used to transform the groups you will animate. */
            transform3d[] transforms;

            /** used to translate the groups you will animate. */
            vector3f[] translations;

            /** type of event for which groups will react. */
            wakeuponawtevent wake;

            /** increments 1 every time the user hits a key. */
            int hitcount;

            /** decides which group will be animated based on the hitcount. */
            int bodypartindex;            
        

Constructor

            simpletripodmovement(transformgroup... groups) {
                this.groups = groups; // you can add a groups count security check if you will

                wake = new wakeuponawtevent(keyevent.key_pressed); // you decide which key later

                translations = new vector3f[groups.length];
                transforms = new transform3d[groups.length];

                for (int i = 0; i < groups.length; i++) {
                    translations[i] = new vector3f(0f, 0f, 0f);
                    transforms[i] = new transform3d();
                }
            }            
        

Implementation of  initialize 

            public void initialize() { // overriden method
                wakeupon(wake); // inherited method
            }
        

Implementation of  processstimulus 

            public void processstimulus(enumeration enumeration) {
                keyevent k = (keyevent) wake.getawtevent()[0];

                /* moves only if the key pressed is the right directional key and 
                   if the hit count is a multiple of 4 */
                if ((k.getkeycode() == keyevent.vk_right) && (hitcount++ % 4 == 0)) {

                    /* selects the body part to be moved */
                    bodypartindex = (bodypartindex + 1) % 3; 

                    /* moves 0.1 on z axis */
                    translations[bodypartindex].set(translations[bodypartindex].x,
                                                    translations[bodypartindex].y,
                                                    translations[bodypartindex].z + 0.1f); 

                    transforms[bodypartindex].settranslation(translations[bodypartindex]);
                    groups[bodypartindex].settransform(transforms[bodypartindex]);
                }


                /* if you don't put it here, it won't respond the next time you press a key */
                wakeupon(wake); 
            }             
        

Applying the behavior

            /**
             * adds a simple tripod movement to the given roach. 
             *
             * @param parts parts that will be animated
             * @param roach supernode of parts
             * @param bounds world bounds, the smae used for lighting 
             */
            void addbehavior(transformgroup[] parts, transformgroup roach, bounds bounds) {
                behavior behavior = new cockroachbehavior(parts);

                /* behavior will not work if you don't set the scheduling bounds! */
                behavior.setschedulingbounds(bounds);

                roach.addchild(behavior);
            }            
        

As you have probably noticed, this class is tightly coupled with the objects it animates, but that is predictable; from  behavior  's javadoc:  the application must provide the behavior object with references to those scene graph elements that the behavior object will manipulate. The application provides those references as arguments to the behavior's constructor when it creates the behavior object. alternatively, the behavior object itself can obtain access to the relevant scene graph elements either when java 3d invokes its initialize method or each time java 3d invokes its processstimulus method. 

Recommended reading:  behavior javadoc 

Resources

  •  cockroach object  [you may have to convert it]
  •  cockroach wings texture 
  •  cockroach head texture 
  •  ground texture 
  •  leaves background 
  •  source code  [you will need to download the model separately]
  •  executable jar  [you will need to download the model separately]
  •  executable jar + model 
  •  complete project  [src + resources]

References

  •  java3d javadoc 
  •  com.sun.j3d.* packages javadoc
  •  java3d application tutorial from sun 
  •  a basic hierarchical model of the top part of a human torso 
Java (programming language) Light (web browser) Object (computer science) Interaction Javadoc Graph (Unix) application Texture (app)

Published at DZone with permission of Dalton Filho. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How To Check for JSON Insecure Deserialization (JID) Attacks With Java
  • Refactoring Java Application: Object-Oriented And Functional Approaches
  • JVM Memory Architecture and GC Algorithm Basics
  • High-Performance Java Serialization to Different Formats

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!