how to get a specific material from a meshrenderer using raycasts - c#

I have a Meshrenderer with 9 materials on it and I want to detect which one of those colors is facing forward, but I don't know which one it is going to be so I can't just use Meshrenderer.materials[number].color
here is what I have so far
public GameObject Shot1pos;
RaycastHit hit;
if (Physics.Raycast(Shot1pos.transform.position, Shot1pos.transform.TransformDirection(Vector3.forward), out hit, Mathf.Infinity))
{
Debug.DrawRay(Shot1pos.transform.position, Shot1pos.transform.TransformDirection(Vector3.forward) * hit.distance, Color.yellow);
Wheel1Color = hit.collider.gameObject.GetComponent<MeshRenderer>().material.color;
Debug.Log(Wheel1Color);
}
but this only returns the first material, whichever is facing forward

I found the solution. First of all you need to get the mesh index according to hit.triangleIndex and get the main material index based on it.
For first one just copy this script in class:
public static int GetSubMeshIndex(Mesh mesh, int triangleIndex)
{
if (!mesh.isReadable) return 0;
var triangleCounter = 0;
for (var subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; subMeshIndex++)
{
var indexCount = mesh.GetSubMesh(subMeshIndex).indexCount;
triangleCounter += indexCount / 3;
if (triangleIndex < triangleCounter) return subMeshIndex;
}
return 0;
}
Then use this part for detecting Suitable material:
public void DetectMaterial(RaycastHit hit)
{
if (hit.transform == null) return;
var materials = hit.transform.GetComponent<MeshRenderer>().materials;
var index = hit.triangleIndex;
var mesh = hit.transform.GetComponent<MeshFilter>().mesh;
var subMeshIndex = GetSubMeshIndex(mesh, index);
UnityEngine.Debug.Log(materials[subMeshIndex]);
}
Remember that your file model must have Read / Write Enabled option enabled and use only Mesh Collider.

Related

How to get Panel to show when character lands on a specific position on game board after dice roll in Unity

I have a 3D board game in Unity. I would like to move my character without having to press a key, but most importantly I would like to show a dynamic panel in canvas for whatever square the character lands on. So far I have the dice rolling and the character moving (after pressing a key) the correct amount of squares, but I am unable to figure out how to activate the panel based on the square color. Any help would be appreciated.
Here is my CharacterScript:
public class CharacterScript : MonoBehaviour
{
public Path currentPath;
public int squarePosition;
public int squares;
bool isMoving;
public GameObject PinkSquarePanel = GameObject.FindGameObjectWithTag("PinkSquare");
public GameObject CyanSquarePanel = GameObject.FindGameObjectWithTag("CyanSquare");
public GameObject WhiteSquarePanel = GameObject.FindGameObjectWithTag("WhiteSquare");
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape) && !isMoving)
{
squares = DiceNumberTextScript.diceNumber;
}
StartCoroutine(Move());
if (squares == 0)
{
ActivateSquarePanel();
}
}
IEnumerator Move()
{
if (isMoving)
{
yield break;
}
isMoving = true;
while (squares > 0)
{
Vector3 nextPosition = currentPath.squareList[squarePosition + 1].position;
while (MoveToNextSquare(nextPosition))
{
yield return null;
}
yield return new WaitForSeconds(0.1f);
squares--;
squarePosition++;
}
isMoving = false;
}
bool MoveToNextSquare(Vector3 goal)
{
return goal != (transform.position = Vector3.MoveTowards(transform.position, goal, 4f * Time.deltaTime));
}
void ActivateSquarePanel()
{
if (squarePosition.Equals(PinkSquarePanel))
{
PinkSquarePanel.GetComponent<CanvasGroup>().alpha = 1;
}
else if (squarePosition.Equals(CyanSquarePanel))
{
CyanSquarePanel.GetComponent<CanvasGroup>().alpha = 1;
}
else if (squarePosition.Equals(WhiteSquarePanel))
{
WhiteSquarePanel.GetComponent<CanvasGroup>().alpha = 1;
}
}
}
And here is my PathScript:
public class Path : MonoBehaviour
{
Transform[] squareObjects;
public List<Transform> squareList = new List<Transform>();
GameObject[] PinkSquares = GameObject.FindGameObjectsWithTag("PinkSquare");
PinkSquare[] pinkList = new PinkSquare[1];
GameObject[] CyanSquares = GameObject.FindGameObjectsWithTag("CyanSquare");
CyanSquare[] cyanList = new CyanSquare[1];
GameObject[] WhiteSquares = GameObject.FindGameObjectsWithTag("WhiteSquare");
WhiteSquare[] whiteList = new WhiteSquare[1];
private void OnDrawGizmos()
{
Gizmos.color = Color.black;
FillSquares();
for (int i = 0; i < squareList.Count; i++)
{
Vector3 currentPosition = squareList[i].position;
if (i > 0)
{
Vector3 previousPosition = squareList[i - 1].position;
Gizmos.DrawLine(previousPosition, currentPosition);
if(currentPosition.Equals(PinkSquares))
{
pinkList[i] = new PinkSquare();
}
else if (currentPosition.Equals(CyanSquares))
{
cyanList[i] = new CyanSquare();
}
else if (currentPosition.Equals(WhiteSquares))
{
whiteList[i] = new WhiteSquare();
}
}
}
}
void FillSquares()
{
squareList.Clear();
squareObjects = GetComponentsInChildren<Transform>();
foreach (Transform square in squareObjects)
{
if (square != this.transform)
{
squareList.Add(square);
}
}
}
}
I believe your issue is in your comparisons, you are trying to use an Equals to compare your currentPosition which is a Vector3 type to a GameObject[] which is an array of gameObjects. As I mentioned in my comments, this comparison will always fail as an array of gameObjects can not be equal to a vector.
Instead of using these lines, try this line:
if(squareList[i].gameObject.tag.CompareTag("PinkSquare")
The full snippet of if else would look like
if(squareList[i].gameObject.tag.CompareTag("PinkSquare")
{
pinkList[i] = new PinkSquare();
}
else if(squareList[i].gameObject.tag.CompareTag("CyanSquare")
{
cyanList[i] = new CyanSquare();
}
else if(squareList[i].gameObject.tag.CompareTag("WhiteSquare")
{
whiteList[i] = new WhiteSquare();
}
Your CharacterScript is going to need to get the gameObject or Transform from the Path script as it is only keeping track of indexes. Your issue in this script is you are comparing an integer to a GameObject which would never be true. I would also recommend not using OnDrawGizmos() as it is an editor only script and should only be used to render editor debugging tools. The only reference to a Gizmo I see in the function is Gizmos.color = Color.black; which does nothing as you are not rendering a gizmo anywhere. I would move this code to a different function and call it from your CharacterScript. Have the return type be GameObject or Transform of the square you are on, so the CharacterSCript can check which color it lands on. Using an Integer nor Vector3 to compare to a GameObject[] will never work.
I am not sure if there are issues elsewhere in the code, but as this comparison would always fail, none of these statements would get broken into. What this means is your panels would never have the chance to get their alpha set nor get created.

Line Renderer Incorrect display

I have a small dilemma and I can't figure out what the problem is. I have a grid of cells ... I drag the mouse through them and create a connection between them with line renderer. The problem is that Line Renderer does not look ok, I will attach an image below to see what the problem is. Thank you very much.
public Grid grid;
public float connectionDistanceLimit = 3.185f; //0.91*3.5
public float snapDistance = 0.2f;
public LineRenderer connectionRenderer;
private List<Vector3> connectionPositions;
public List<GridCell> cellsOnScreen = new List<GridCell>();
public List<GridCell> connectedCells;
private GridCell lastConnectedCell;
private GridCell currentSelectedCell;
private bool conectionStarted = false;
IEnumerator Start()
{
yield return new WaitForEndOfFrame();
cellsOnScreen = grid.GetGridElements();
foreach (GridCell cell in cellsOnScreen)
cell.CellPointEvent += Cell_CellPointEvent;
}
private void Cell_CellPointEvent(bool enter, GridCell cell)
{
currentSelectedCell = cell;
if (conectionStarted)
return;
connectionPositions = new List<Vector3>();
connectedCells = new List<GridCell>();
connectedCells.Add(cell);
Vector3 positionToAdd = cell.transform.position;
positionToAdd.z = 0f;
connectionPositions.Add(positionToAdd);
cell.SetColor();
conectionStarted = true;
}
void Update()
{
if (Input.GetMouseButton(0))
{
if (connectionPositions != null)
{
if (lastConnectedCell != null)
{
if (lastConnectedCell != currentSelectedCell)
return;
}
List<Vector3> tempPositions = new List<Vector3>(connectionPositions);
Vector3 mPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector3 lastConnection = tempPositions[tempPositions.Count - 1];
mPosition.z = 0f;
lastConnection.z = 0f;
if (Vector3.Distance(lastConnection, mPosition) > connectionDistanceLimit)
{
Ray r = new Ray(lastConnection, (mPosition - lastConnection));
mPosition = r.GetPoint(connectionDistanceLimit);
}
if (Vector3.Distance(lastConnection, mPosition) < connectionDistanceLimit)
foreach (GridCell cell in cellsOnScreen)
{
if (!connectedCells.Contains(cell))
{
GridCell lastCell = connectedCells[connectedCells.Count - 1];
if ((cell.transform.position.y > lastCell.transform.position.y && cell.transform.position.x != lastCell.transform.position.x) ||
cell.transform.position.y < lastCell.transform.position.y && cell.transform.position.x != lastCell.transform.position.x)
{
}
else
{
if (Vector3.Distance(mPosition, cell.transform.position) <= snapDistance)
{
connectedCells.Add(cell);
connectionPositions.Add(cell.transform.position);
cell.isConnected = true;
cell.SetColor();
break;
}
}
}
}
lastConnection.z = 0f;
mPosition.z = 0f;
tempPositions.Add(mPosition);
connectionRenderer.positionCount = tempPositions.Count;
connectionRenderer.SetPositions(tempPositions.ToArray());
}
}
if (Input.GetMouseButtonUp(0))
{
lastConnectedCell = connectedCells[connectedCells.Count - 1];
}
}
In the inspector I look ok, but in Play Mode it makes this effect that I don't realize why. I thought position Z was a problem, but no, it's 0.
Sorry for bad English.
I'm 7 months late, but it looks like your width is too small. I was just trying to debug this myself and the unity line renderer really doesn't play nice with low widths and closely adjacent positions. you either need to increase the width or find another solution to draw the line you want.
i recommend just computing the vertex positions yourself and rendering a model that way tho you can probably find some stuff on the unity store
That weird behaviour might happen if you have multiple points positions at one place, or width curve's wrong setup.
example

Move Gameobject generating Objects based on Data

I am generating gameobjects (spheres) based on coordinates, which are stored in a .csv file. I have a Gameobject with a Single Sphere as primitive childobject. Based on the data the Object will clone this sphere 17 times and move them around. I can move the whole thing around like i want it to by accessing the parent object, but in editing mode the position of the root sphere makes it uneasy to use.
The following Code makes this possible.
public GameObject parentObj;
public TextAsset csvFile;
[SerializeField]
private float scaleDownFactor = 10;
private int index = 0;
//class Deck : MonoBehaviour
//{
[SerializeField]
private GameObject[] deck;
private GameObject[] instanciatedObjects;
private void Start()
{
Fill();
}
public void Fill()
{
instanciatedObjects = new GameObject[deck.Length];
for (int i = 0; i < deck.Length; i++)
{
instanciatedObjects[i] = Instantiate(deck[i]) as GameObject;
}
}
//}
// Update is called once per frame
void Update()
{
readCSV();
}
void readCSV()
{
string[] frames = csvFile.text.Split('\n');
int[] relevant = {
0
};
string[] coordinates = frames[index].Split(',');
for (int i = 0; i < 17; i++)
{
float x = float.Parse(coordinates[relevant[i] * 3]) / scaleDownFactor;
float y = float.Parse(coordinates[relevant[i] * 3+1]) / scaleDownFactor;
float z = float.Parse(coordinates[relevant[i] * 3+2]) / scaleDownFactor;
//objectTest.transform.Rotate(float.Parse(fields[1]), float.Parse(fields[2]), float.Parse(fields[3]));
//objectTest.transform.Translate(x, y, z);
//parentObj.transform.position = new Vector3(x, y, z);
instanciatedObjects[i].transform.position = new Vector3(parentObj.transform.position.x, parentObj.transform.position.y, parentObj.transform.position.z);
instanciatedObjects[i].transform.eulerAngles = new Vector3(parentObj.transform.eulerAngles.x, parentObj.transform.eulerAngles.y, parentObj.transform.eulerAngles.z);
//instanciatedObjects[i].transform.position = new Vector3(x, y, z);
instanciatedObjects[i].transform.Translate(x, y, z);
}
if (index < frames.Length - 1)
{
index++;
}
if (index >= frames.Length -1)
{
index = 0;
}
}
Here is a Screenshot:
So my question is:
How can I set the Position of this Sphere to one of the moving points, without changing the position of the cloned objects? Since all behave based on the BaseSphere?
Is it possible to make the BaseSphere not visible While the Objects are getting cloned or generated?
I am looking for a solution, that makes it easier to move the datagenerated Object around in Editor.
I would appreciate any kind of input.
Make all Spheres children of Empty Gameobject (for example. Sphere_root) and use this for moving Spheres.
Also check Scriptable Objects. It is simple and very quick method to manage data in Unity.
#Edit
public void Fill()
{
instanciatedObjects = new GameObject[deck.Length];
for (int i = 0; i < deck.Length; i++)
{
instanciatedObjects[i] = Instantiate(deck[i]) as GameObject;
instanciatedObjects[i].transform.parent = Baumer; // or Sphere Root or somehing else.
}
}

Unity, get the "actual" current Terrain?

Unity has a function Terrain.sampleHeight(point) which is great, it instantly gives you the height of the Terrain underfoot rather than having to cast.
However, any non-trivial project has more than one Terrain. (Indeed any physically large scene inevitably features terrain stitching, one way or another.)
Unity has a function Terrain.activeTerrain which - I'm not making this up - gives you: the "first one loaded"
Obviously that is completely useless.
Is fact, is there a fast way to get the Terrain "under you"? You can then use the fast function .sampleHeight ?
{Please note, of course, you could ... cast to find a Terrain under you! But you would then have your altitude so there's no need to worry about .sampleHeight !}
In short is there a matching function to use with sampleHeight which lets that function know which Terrain to use for a given xyz?
(Or indeed, is sampleHeight just a fairly useless demo function, usable only in demos with one Terrain?)
Is there in fact a fast way to get the Terrain "under you" - so as to
then use the fast function .sampleHeight ?
Yes, it can be done.
(Or indeed, is sampleHeight just a fairly useless demo function,
usable only in demos with one Terrain?)
No
There is Terrain.activeTerrain which returns the main terrain in the scene. There is also Terrain.activeTerrains (notice the "s" at the end) which returns active terrains in the scene.
Obtain the terrains with Terrain.activeTerrains which returns Terrain array then use Terrain.GetPosition function to obtain its position. Get the current terrain by finding the closest terrain from the player's position. You can do this by sorting the terrain position, using Vector3.Distance or Vector3.sqrMagnitude (faster).
Terrain GetClosestCurrentTerrain(Vector3 playerPos)
{
//Get all terrain
Terrain[] terrains = Terrain.activeTerrains;
//Make sure that terrains length is ok
if (terrains.Length == 0)
return null;
//If just one, return that one terrain
if (terrains.Length == 1)
return terrains[0];
//Get the closest one to the player
float lowDist = (terrains[0].GetPosition() - playerPos).sqrMagnitude;
var terrainIndex = 0;
for (int i = 1; i < terrains.Length; i++)
{
Terrain terrain = terrains[i];
Vector3 terrainPos = terrain.GetPosition();
//Find the distance and check if it is lower than the last one then store it
var dist = (terrainPos - playerPos).sqrMagnitude;
if (dist < lowDist)
{
lowDist = dist;
terrainIndex = i;
}
}
return terrains[terrainIndex];
}
USAGE:
Assuming that the player's position is transform.position:
//Get the current terrain
Terrain terrain = GetClosestCurrentTerrain(transform.position);
Vector3 point = new Vector3(0, 0, 0);
//Can now use SampleHeight
float yHeight = terrain.SampleHeight(point);
While it's possible to do it with Terrain.SampleHeight, this can be simplified with a simple raycast from the player's position down to the Terrain.
Vector3 SampleHeightWithRaycast(Vector3 playerPos)
{
float groundDistOffset = 2f;
RaycastHit hit;
//Raycast down to terrain
if (Physics.Raycast(playerPos, -Vector3.up, out hit))
{
//Get y position
playerPos.y = (hit.point + Vector3.up * groundDistOffset).y;
}
return playerPos;
}
Terrain.GetPosition() = Terrain.transform.position = position in world
working method:
Terrain[] _terrains = Terrain.activeTerrains;
int GetClosestCurrentTerrain(Vector3 playerPos)
{
//Get the closest one to the player
var center = new Vector3(_terrains[0].transform.position.x + _terrains[0].terrainData.size.x / 2, playerPos.y, _terrains[0].transform.position.z + _terrains[0].terrainData.size.z / 2);
float lowDist = (center - playerPos).sqrMagnitude;
var terrainIndex = 0;
for (int i = 0; i < _terrains.Length; i++)
{
center = new Vector3(_terrains[i].transform.position.x + _terrains[i].terrainData.size.x / 2, playerPos.y, _terrains[i].transform.position.z + _terrains[i].terrainData.size.z / 2);
//Find the distance and check if it is lower than the last one then store it
var dist = (center - playerPos).sqrMagnitude;
if (dist < lowDist)
{
lowDist = dist;
terrainIndex = i;
}
}
return terrainIndex;
}
It turns out the answer is simply NO, Unity does not provide such a function.
You can use this function to get the Closest Terrain to your current Position:
int GetClosestTerrain(Vector3 CheckPos)
{
int terrainIndex = 0;
float lowDist = float.MaxValue;
for (int i = 0; i < _terrains.Length; i++)
{
var center = new Vector3(_terrains[i].transform.position.x + _terrains[i].terrainData.size.x / 2, CheckPos.y, _terrains[i].transform.position.z + _terrains[i].terrainData.size.z / 2);
float dist = Vector3.Distance(center, CheckPos);
if (dist < lowDist)
{
lowDist = dist;
terrainIndex = i;
}
}
return terrainIndex;
}
and then you can use the function like this:
private Terrain[] _terrains;
void Start()
{
_terrains = Terrain.activeTerrains;
Vector3 start_pos = Vector3.zero;
start_pos.y = _terrains[GetClosestTerrain(start_pos)].SampleHeight(start_pos);
}
public static Terrain GetClosestTerrain(Vector3 position)
{
return Terrain.activeTerrains.OrderBy(x =>
{
var terrainPosition = x.transform.position;
var terrainSize = x.terrainData.size * 0.5f;
var terrainCenter = new Vector3(terrainPosition.x + terrainSize.x, position.y, terrainPosition.z + terrainSize.z);
return Vector3.Distance(terrainCenter, position);
}).First();
}
Raycast solution: (this was not asked, but for those looking for Solution using Raycast)
Raycast down from Player, ignore everything that has not Layer of "Terrain" (Layer can be easily set in inspector).
Code:
void Update() {
// Put this on Player! Raycast's down (raylength=10f), if we hit something, check if the Layers name is "Terrain", if yes, return its instanceID
RaycastHit hit;
if (Physics.Raycast (transform.localPosition, transform.TransformDirection (Vector3.down), out hit, 10f, 1 << LayerMask.NameToLayer("Terrain"))) {
Debug.Log(hit.transform.gameObject.GetInstanceID());
}
}
At this point already, you have a reference to the Terrain by "hit.transform.gameObject".
For my case, i wanted to reference this terrain by its instanceID:
// any other script
public static UnityEngine.Object FindObjectFromInstanceID(int goID) {
return (UnityEngine.Object)typeof(UnityEngine.Object)
.GetMethod("FindObjectFromInstanceID", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
.Invoke(null, new object[] { goID });
}
But as written above, if you want the Terrain itself (as Terrain object) and not the instanceID, then "hit.transform.gameObject" will give you the reference already.
Input and code snippets taken from these links:
https://answers.unity.com/questions/1164722/raycast-ignore-layers-except.html
https://answers.unity.com/questions/34929/how-to-find-object-using-instance-id-taken-from-ge.html

Material determination of submeshes through raycasting in Unity3d

I'm attempting to place props randomly on a mesh. The mesh has 2 materials and I'd like to only place the props on one of the materials. I'm currently doing this through raycasting, determining the material at the point hit and if the material is correct I place a prop at that point. I'm having an issue where the material is being determined incorrectly and I'm not certain why. Code is below.
using UnityEngine;
public class BoundingBoxTry : MonoBehaviour {
// Use this for initialization
public MeshFilter meshFilter;
private Renderer rend;
private Mesh mesh;
private Bounds bnds;
private float nextX, nextZ, maxX, minX, maxZ, minZ;
public GameObject rayOrigin;
public GameObject propPreFab;
private MeshCollider thisCollider;
void Start () {
thisCollider = GetComponentInChildren<MeshCollider>();
rend = GetComponentInChildren<Renderer>();
mesh = GetComponentInChildren<MeshFilter>().sharedMesh;
bnds = rend.bounds;
}
void Update () {
nextX = Random.Range(0, bnds.size.x);
nextZ = Random.Range(0, bnds.size.z);
nextX = nextX-bnds.size.x/2;
nextZ = nextZ-bnds.size.z/2;
nextX = bnds.center.x-nextX;
nextZ = bnds.center.z-nextZ;
RaycastHit hit = new RaycastHit();
Vector3 randomCoord = new Vector3(nextX, 0, nextZ);
rayOrigin.transform.position = new Vector3(nextX, 100, nextZ);
bool didHit = Physics.Raycast(rayOrigin.transform.position, -new Vector3(0, 1, 0), out hit);
int vertCounter = 0;
if(didHit && hit.collider == thisCollider){
for (int sMC = 0; sMC < mesh.subMeshCount; sMC++)
{
int[] subMesh = mesh.GetTriangles(sMC);
vertCounter = vertCounter + subMesh.Length;
if(hit.triangleIndex < vertCounter){
if(rend.sharedMaterials[sMC] == rayOrigin.GetComponent<Renderer>().sharedMaterial){
GameObject prop = Instantiate(propPreFab, hit.point, Quaternion.identity);
}
else{
}
}
}
}
}
}
I've also attached a picture to demonstrate the behavior I'm seeing.
Of course, as soon as I ask and sleep on the problem I'm able to figure it out. Arman's link was helpful as well. The issue I was seeing was caused by my not fully understanding the data I was getting from the triangleIndex call.
When calling triangleIndex you receive a vertex index back. To get the triangle hit from that vertex you must multiply the vertex index by 3 and then validate which subMesh was struck by the raycast.

Categories

Resources