[SOLVED] C# - Rotating Instantiated Objects Based on Terrain

I’ve written a series of scripts that randomize content in my game based on biomes assigned to a grid-like wilderness where each ‘tile’ is a scene. Everything’s going great so far, it can create the terrain objects, scatter things like trees and rocks appropriately, places them at terrain height so they’re not buried, and even rotates these objects randomly along the Y axis. But what I REALLY want it to do is also rotate them along the X and Z based on the height of the terrain at the edges of the object.

For example, if I have a fallen log. Right now, the script places it at the height of the terrain at the center of the log. But if it happens to be placed on a hill, half of the log is buried in the hill and the other half sticks out into thin air! I want it to instead lay flat along the slope of the hill.

I’m not really sure where to even start with this. I think it’ll have something to do with the Renderer.size. I believe I’ll basically need to get the angle between the four furthest points along both X and Z axis and then create a new rotation for the object based on those. But I’m still really hazy on how exactly Quaternions work, for example, and not sure where to go from here.

For reference, here is the script that I have which is creating and placing the objects right now:

using UnityEngine;
using System.Collections;

public class SpawnObjects : MonoBehaviour
{
    public Terrain terrain; // add current terrain
    public string[] objectToPlace; // list of objects to be placed on terrain
    public string eventToPlace; // what event we're placing, if we are
    public GameObject placingObject; // the individual object we're currently placing
    public int numberOfObjects; // number of how many objects will be created
    public int posMin; // minimum y position
    public int posMax; // maximum x position
    public bool posMaxIsTerrainHeight; // the maximum height is the terrain height
    public bool isEvent; // determine if we're placing an object or an event
    public int numberOfPlacedObjects; // number of the plaed objects
    private int terrainWidth; // terrain size x axis
    private int terrainLength; // terrain size z axis
    private int terrainPosX; // terrain position x axis
    private int terrainPosZ; // terrain position z axis
    private float yDisplace; // how far to move the object above the terrain

    AllObjects items;

    // Create objects on the terrain with random positions
    public void PlaceObject()
    {
        items = GetComponent<AllObjects> ();
        terrainWidth = 500; // get terrain size x
        terrainLength = 500; // get terrain size z
        terrainPosX = 0; // get terrain position x
        terrainPosZ = 0; // get terrain position z
        if(posMaxIsTerrainHeight == true)
        {
            posMax = (int)terrain.terrainData.size.y;
        }

        int posx = Random.Range(terrainPosX, terrainPosX + terrainWidth); // generate random x position
        int posz = Random.Range(terrainPosZ, terrainPosZ + terrainLength); // generate random z position
        float posy = Terrain.activeTerrain.SampleHeight(new Vector3(posx, 0, posz)); // get the terrain height at the random position
        if (isEvent) {
            int r = Random.Range (0, items.items [eventToPlace].Length); // pick a random event from the list of possibles
            placingObject = items.items [eventToPlace] [r]; // set prefab as event obj we're working with
            if (posy <= posMax && posy >= posMin) {
                yDisplace = (placingObject.GetComponent<Renderer> ().bounds.size.y) / 2; // get height of object so it doesn't spawn half-buried
                Instantiate (placingObject, new Vector3 (posx, posy + yDisplace, posz), Quaternion.AngleAxis (Random.Range (0, 360), Vector3.up)); // create object
            }
        } else {
            int r = Random.Range (0, objectToPlace.Length); // pick object type from scatterables list
            int i = Random.Range (0, items.items [objectToPlace [r]].Length); // select prefab from list of items of that type
            placingObject = items.items [objectToPlace [r]] [i]; // set prefab as obj we're working with
            if (posy <= posMax && posy >= posMin) {
                yDisplace = (placingObject.GetComponent<Renderer> ().bounds.size.y) / 2; // get height of object so it doesn't spawn half-buried
                Instantiate (placingObject, new Vector3 (posx, posy + yDisplace, posz), Quaternion.AngleAxis (Random.Range (0, 360), Vector3.up)); // create object
                numberOfPlacedObjects++;
            }

            // numberOfPlacedObjects is smaller than numberOfObjects
            if (numberOfPlacedObjects < numberOfObjects) {
                PlaceObject (); // place another one
            }
        }
    }
}

If anyone can please point me in the right direction that would be awesome! Thanks!

This is fairly straightforward if you don’t care about the Y orientation of your object, or you have a separate sub-GameObject that can control its “heading” separately from the root of things.

What you do is take three raycasts downward in an equilateral triangle from around the center point of where you are placing the object, and you get three separate points of intersection with the TerrainCollider. You can use layers to ensure you are getting the collider you want.

Those three points can be used to create a Unity3D Plane() object. One of the accessible methods of that Plane() object is its normal, which is the vector normal to the plane. Hey, guess what, that is facing UP.

The next thing you do is tell your emplaced tree object, “Look such that your UP is in the direction of this normal.”

That sounds simple, but there is an extra step you have to do when you use the transform.LookAt() method to orient your tree: generally your tree will have its Y axis facing “up,” and when you do a .LookAt() it will cause the Z axis to look up, so your trees will be handily lying on the ground. Not useful unless you are modeling the forest around the Tunguska Blast zone in Russia.

You COULD go hammer all your prefabs together 90 degrees off but that would be silly and your artists would probably stage a riot.

Instead, you can synthesize a baseline from the point between two of your points (average of vectors) to the other point, sort of a “this is the line I’m standing on” baseline. Then you use the .LookAt() method to s “Look ALONG this baseline, but consider your “up” to be the plane normal.” This uses the two-argument version of .LookAt() to supply the “up” portion.

Putting this all together, this is what you want:

Plane p = new Plane (TL.position, TR.position, TAFT.position);
Vector3 fwd = (TL.position + TR.position) * 0.5f - TAFT.position;
transform.LookAt (transform.position + fwd, p.normal);

The above code is from my tank game but TL and TR are transforms corresponding to the front left and right corners of my tank, and TAFT is the center back. “fwd” is the baseline mentioned above. But it works regardless of how you get the three positions involved. You do NOT have to store them in a transform, just store them in three Vector3 objects, say p1, p2, and p3.

Thanks so much for your help! While I didn’t use the solution you offered, exactly, I did some digging around on the magic words you gave me and ended up using this solution:

            if (posy <= posMax && posy >= posMin) {
                yDisplace = ((placingObject.GetComponent<Renderer> ().bounds.size.y) / 2); // get height of object so it doesn't spawn half-buried
                placeAt = new Vector3 (posx, posy + yDisplace, posz); // determine where to place it
                var obj = Instantiate (placingObject, placeAt, Quaternion.AngleAxis (Random.Range (0, 360), Vector3.up)); // create the object
                RaycastHit hit;
                var ray = new Ray (obj.transform.position, Vector3.down); // check for slopes
                if (terrain.GetComponent<Collider>().Raycast(ray, out hit, 1000)) {
                    obj.transform.rotation = Quaternion.FromToRotation(obj.transform.up, hit.normal)*obj.transform.rotation; // adjust for slopes
                }
            }

This is my first time using Raycast so I never would’ve known to look if you didn’t point me that way!

Ah cool. Your solution is rather more straight to the point… I forgot that a big part of my solution was so that the tank could “roll along and over” lumps dynamically, “straddling” larger features realistically, which is totally unnecessary for your application. Good choice.