- Home /
yield in for loop breaking render texture
My first snippet of code works correctly. It is a function with a for loop that runs 10 times. Each time the loop runs a camera is drawn to a render texture then put in a texture2d. Then there are two more for loops that look though every pixel of the texture2d looking for green pixels.
void FindBestSpot(){
for (int i = 0; i < 10; i++) {
FrameObject ();
RenderTexture renderTexture = new RenderTexture (snapShotWidth / 4, snapShotHeight / 4, 24);
cam.targetTexture = renderTexture;
Texture2D snapShot = new Texture2D (snapShotWidth / 4, snapShotHeight / 4, TextureFormat.RGB24, false);
cam.Render ();
RenderTexture.active = renderTexture;
snapShot.ReadPixels (new Rect (0, 0, snapShotWidth / 4, snapShotHeight / 4), 0, 0);
cam.targetTexture = null;
RenderTexture.active = null;
Destroy (renderTexture);
int totalPixels = 0;
int greenPixels = 0;
for (int x = 0; x < snapShot.width; x++) {
for (int y = 0; y < snapShot.height; y++) {
totalPixels++;
if (snapShot.GetPixel (x, y) == Color.green) {
greenPixels++;
}
}
}
float percentCoverage = (float)greenPixels / (float)totalPixels;
snapShotSpots.Add (new SnapShotSpot (cam.transform.position, percentCoverage));
}
SnapShotSpot bestSpot = snapShotSpots[0];
float bestPercent = snapShotSpots[0].percentCoverage;
foreach (SnapShotSpot spot in snapShotSpots) {
if (spot.percentCoverage > bestPercent) {
bestSpot = spot;
bestPercent = spot.percentCoverage;
}
}
foreach (Uid uid in cloneUids) {
DestroyImmediate (uid.gameObject);
}
cloneUids.Clear ();
bestCamSpot = bestSpot;
}
The only problem with this code is that it takes too long to perform smoothly in one frame. So I tried to make the function a coroutine and add a yield at the end of the for loop.
IEnumerator FindBestSpot(){
for (int i = 0; i < 10; i++) {
FrameObject ();
RenderTexture renderTexture = new RenderTexture (snapShotWidth / 4, snapShotHeight / 4, 24);
cam.targetTexture = renderTexture;
Texture2D snapShot = new Texture2D (snapShotWidth / 4, snapShotHeight / 4, TextureFormat.RGB24, false);
cam.Render ();
RenderTexture.active = renderTexture;
snapShot.ReadPixels (new Rect (0, 0, snapShotWidth / 4, snapShotHeight / 4), 0, 0);
cam.targetTexture = null;
RenderTexture.active = null;
Destroy (renderTexture);
int totalPixels = 0;
int greenPixels = 0;
for (int x = 0; x < snapShot.width; x++) {
for (int y = 0; y < snapShot.height; y++) {
totalPixels++;
if (snapShot.GetPixel (x, y) == Color.green) {
greenPixels++;
}
}
}
float percentCoverage = (float)greenPixels / (float)totalPixels;
Debug.Log(greenPixels + "/" + totalPixels + "=" + percentCoverage);
snapShotSpots.Add (new SnapShotSpot (cam.transform.position, percentCoverage));
//yield return null;
yield return new WaitForSeconds(0.1f);
}
SnapShotSpot bestSpot = snapShotSpots[0];
float bestPercent = snapShotSpots[0].percentCoverage;
foreach (SnapShotSpot spot in snapShotSpots) {
if (spot.percentCoverage > bestPercent) {
bestSpot = spot;
bestPercent = spot.percentCoverage;
}
}
foreach (Uid uid in cloneUids) {
DestroyImmediate (uid.gameObject);
}
cloneUids.Clear ();
bestCamSpot = bestSpot;
}
I tried making the for loop yield for 1 frame as well as for a set amount of time and in both cases it stop functioning correctly. Anytime there is a yield in the for loop the function only works correctly the first time through and the other 9 times the debug statement return that there a 0 green pixels in the texture2d. It returns the correct amount of totalPixels every time. Even when I have the code write the images to files the image files appear to be correct. There are green pixels in the image file but the code still returns 0 green pixels every time except the first time through.
Is there a reason why this is happening? Why would the image file appear correct but the code still not find any green pixels in the texture2d? Why would this work correctly when the for loop runs 10 times in one frame but not work correctly when it runs once per frame? Is there any other way to spread this for loop over time so the game doesn't freeze while this is happening? I've looked in to multi-threading possibly using Thread Ninja but almost everything that happens in the loop is using functions dependent on unity engine. From what I understand all of these lines need to happen in the main thread. Is there a way around this or a better way to accomplish what I'm trying to do?
Let me know if you need any more info.
First thing I would do, is rethink the different steps. The second part where you compare the percentCoverage, you can do that when you get the value in the first loop.
float percentCoverage = (float)greenPixels / (float)totalPixels;
Debug.Log(greenPixels + "/" + totalPixels + "=" + percentCoverage);
// snapShotSpots.Add (new SnapShotSpot (cam.transform.position, percentCoverage));
if (percentCoverage > bestPercent) {
bestSpot = spot;
bestPercent = percentCoverage;
}
Second, based on the current best percent coverage, you can already assume whether it is worth continuing with a texture check. This one goes a bit further. Basically, to explain simply, if your best coverage is let's say 51%, you've run 75% of the texture and only has 20%, you can stop. It won't be the most covered image whatsoever. Add a bit of heuristic and you can even anticipate based on average. If the images contains patterns, you can optimize that a lot more. If you know there is one spot with green, after a certain amount of non-green you could deter$$anonymous$$e there is no more (this is only valid if the image contains one shape of green).
You could also try to first set all 10 images and yield. Then back on checking the green on first image, yield and so on. Using a bit of the above advices, maybe you get to run the code properly without looking for why it fails now.
As for your problem, the only missing part on your snippet is the implementation of FrameObject. $$anonymous$$aybe it is in there coz I don't see where snapShot gets assigned.
I'm aware the code can be optimized but there's still no way it will be able to be executed all in one frame. I plan on optimizing it once its functional. FrameObject() shouldn't have any impact.
void FrameObject(){
Bounds bounds = CalculateBounds ();
Vector3 max = bounds.size;
float radius = $$anonymous$$athf.$$anonymous$$ax(max.x, $$anonymous$$athf.$$anonymous$$ax(max.y, max.z));
float distance = (radius / ($$anonymous$$athf.Sin (cam.fieldOfView * $$anonymous$$athf.Deg2Rad / 2f))) * 0.7f;
Vector3 position = Random.onUnitSphere * distance + bounds.center;
int checks = 0;
while (Physics.CheckSphere (position, 0.3f) && checks < 100) {
position = Random.onUnitSphere * distance + bounds.center;
checks++;
}
cam.transform.position = position;
cam.transform.LookAt (bounds.center);
}
Bounds CalculateBounds(){
Bounds bounds = new Bounds (targetUids[0].transform.position, Vector3.zero);
foreach (Uid uid in targetUids) {
bounds.Encapsulate (uid._renderer.bounds);
}
return bounds;
}
snapShot get assigned here.
Texture2D snapShot = new Texture2D (snapShotWidth / 4, snapShotHeight / 4, TextureFormat.RGB24, false);
cam.Render ();
RenderTexture.active = renderTexture;
snapShot.ReadPixels (new Rect (0, 0, snapShotWidth / 4, snapShotHeight / 4), 0, 0);
snapShot.ReadPixels() reads the pixels of the active RenderTexture in to the Texture2d.
After looking closely at the image files I just noticed that the green object is not 100% green. It appears 100% green on the first image and all the subsequent images the object is very close but not (0, 1, 0, 1) green. The script changes the objects material to a material using the unlit/color shader with the color set to (0, 1, 0, 1) green. Is it possible that for the first frame the material is changed and the object is 100% green but after the yield time has passed something has affected the material to make it an off green. I thought the unlit shader would prevent any lighting from changing its color but now I don't know.
You can yield after each image check so it is done over 10 frames. One image per frame (if it is not a 4k image) should be fine. You can go faster by preventing to check all three values for each color.
if(color.g == 1 && color.r == 0&& color.b == 0){}
See in this case, any other color that doe snot have full green channel will be discarded after one float check (use approximate maybe for error margin). Some will pass first check (like white) but may fail on second and only full green will require all three checks.
Piling up all optimisations plus the yield, you should be able to run.
Answer by Bunny83 · Sep 11, 2017 at 07:49 PM
Where do you originally call the method from? Any sort of rendering can only be done while the engine is internally in "render state". Using just WaitForSeconds will resume the coroutine during the "update state". It should work when you do this instead:
yield return new WaitForSeconds(0.1f);
yield return new WaitForEndOfFrame();
So first it waits for the desired time and when the time has passed it waits again for the end of the current frame.
Keep in mind that the time moves on while you wait for the coroutine to continue.
The function is called in the scripts awake() function. The gameobject with the script is instantiated at a certain user action. I added the WaitForEndOfFrame after the the WaitForSeconds but the result is still the same.