playcanvas / engine

JavaScript game engine built on WebGL, WebGPU, WebXR and glTF
https://playcanvas.com
MIT License
9.56k stars 1.34k forks source link

Particle System Screen Space and Local Space not rendered correctly #3724

Open marcusx2 opened 2 years ago

marcusx2 commented 2 years ago

If you choose Screen Space for the particle system and put it under a Screen Entity, the particles will stretch and behave differently than if it was outside the Screen Entity and not using Screen Space. Furthermore, Local Space doesn't work in combination with Screen Space. If I create a group under the Screen Entity, anchor said group to the bottom of the screen, and place the particle system under the group, the particle system won't be anchored to the bottom of the screen, for example.

TL;DR

  1. Particle System stretches and behaves differently under a Screen Entity and using Screen Space.
  2. Local Space not working in conjunction with Screen Space. I can't anchor the particle system anywhere on the screen.
marcusx2 commented 2 years ago

@yaustar See these bubbles image

They are stretched, this only happens if I use Screen Space. I hope this can be fixed soon, I really need to use screen space after all.

If it's an easy patch and you can tell me how I can do it myself, please let me know.

yaustar commented 2 years ago

If you have a public project example that showcases the issue, that will really help if you can link it here please

marcusx2 commented 2 years ago

@yaustar Well it's really simple to reproduce the issue. You just need to use the screenspace image ScreenSpaceIssue.zip

The sample project has 2 particle systems, one using screenspace and the other isn't, so you can see how the bubbles are stretched on screen space.

image

yaustar commented 2 years ago

Project to reproduce the issue: https://playcanvas.com/editor/scene/1276844

https://user-images.githubusercontent.com/16639049/142775855-394ae5e9-4a70-4375-9cf4-883a76ec893c.mp4

There are issues/questions around how to control the bounds of the particle effects as it seems very small when a child of an element.

This issue is also reproducible in the engine examples when changing the rendering viewport size. The particles stretch based on the aspect ratio of the canvas.

https://user-images.githubusercontent.com/16639049/142775903-e38dc8a8-7255-470a-a09d-6a688ea5446d.mp4

marcusx2 commented 2 years ago

I pray it's an easy fix! Thanks for looking into this @yaustar

yaustar commented 2 years ago

Had a quick chat with the team and unfortunately it's not an easy fix unfortunately. In the short term, you would need to implement a workaround. Eg animated UI sprite or a simple particle implementation in the UI space or mapping the UI position to a world space so that the 3D particle system can be moved so that it is always over/under an element

marcusx2 commented 2 years ago

Had a quick chat with the team and unfortunately it's not an easy fix unfortunately. In the short term, you would need to implement a workaround. Eg animated UI sprite or a simple particle implementation in the UI space or mapping the UI position to a world space so that the 3D particle system can be moved so that it is always over/under an element

Do you have an ETA to fix this? It's ok if not, I'd just like to know if there is. It's not an easy fix, but there is a fix right?

yaustar commented 2 years ago

Do you have an ETA to fix this? It's ok if not, I'd just like to know if there is. It's not an easy fix, but there is a fix right?

Afraid not. It's not a straight forward 'this is how we do it'. The solution for it is not yet known and in some ways, a new feature.

I can think of workarounds for this that involve layers and a little scripting that I hope to think about later this week

marcusx2 commented 2 years ago

I can think of workarounds for this that involve layers and a little scripting that I hope to think about later this week

Thanks, I very much appreciate it.

The solution for it is not yet known and in some ways, a new feature.

A new feature? Interesting.

yaustar commented 2 years ago

A new feature? Interesting.

The scope of work is 'large' and also undefined at the moment

yaustar commented 2 years ago

Work around where the VFX follows the element entity and renders on type: https://playcanvas.com/editor/scene/1278415

https://user-images.githubusercontent.com/16639049/143048528-c68e7905-4408-445c-ac85-a42457dc9f99.mp4

It renders on top as it relies on the default UI layer clearing the depth buffer.

If you want to render under the UI layer, you will have create a layer for it that renders before the default UI layer but also has clearDepthBuffer enabled: https://developer.playcanvas.com/en/api/pc.Layer.html#clearDepthBuffer

marcusx2 commented 2 years ago

@yaustar is there a workaround where I can dynamically set the scale of the particles based on the rendering viewport size? So basically I cancel out the scaling that is applied.

yaustar commented 2 years ago

@yaustar is there a workaround where I can dynamically set the scale of the particles based on the rendering viewport size? So basically I cancel out the scaling that is applied.

Not easily, no. You would have to scale it according it based on the viewport size and how the UI screen scales the elements.

Or you could potentially get the canvas corners of the element that it is following and depending on the size of that, use that to scale the particles relatively: https://developer.playcanvas.com/en/api/pc.ElementComponent.html#canvasCorners

marcusx2 commented 2 years ago

@yaustar is there a workaround where I can dynamically set the scale of the particles based on the rendering viewport size? So basically I cancel out the scaling that is applied.

Not easily, no. You would have to scale it according it based on the viewport size and how the UI screen scales the elements.

Or you could potentially get the canvas corners of the element that it is following and depending on the size of that, use that to scale the particles relatively: https://developer.playcanvas.com/en/api/pc.ElementComponent.html#canvasCorners

Sorry I'm a bit lost on this. As I understand the idea you are talking about is to check the original dimensions of the particles, then check the canvas corners to determine how much it was scaled? Do you have an example? To get the viewport size it's app.graphicsDevice.width; and app.graphicsDevice.height right, or are you talking about something else? Thanks!

yaustar commented 2 years ago

I've updated the example above that scales based on the idea above. It's not perfect but gives an idea on the approach

marcusx2 commented 2 years ago

I've made a fix that works perfectly well

var ParticleScalingFix = pc.createScript('particleScalingFix');

/** @type {pc.Vec3}*/
let originalScale;

// initialize code called once per entity
ParticleScalingFix.prototype.initialize = function() {

    originalScale =  { ...this.entity.getLocalScale() };

    this.app.graphicsDevice.on('resizecanvas', this.onResizeCanvas, this);
    this.onResizeCanvas(this.app.graphicsDevice.width, this.app.graphicsDevice.height);
};

ParticleScalingFix.prototype.onResizeCanvas = function(w, h) {
    this.entity.setLocalScale(originalScale.x, (w/h) * originalScale.y, originalScale.z);

};

Just need to attach the script to the particle system.

marcusx2 commented 2 years ago

@yaustar

I just noticed another problem with using the screen space. If you switch from portrait to landscape, and then from landscape to portrait, the particles start moving differently. This seems to be another issue entirely. Do you think this is worth another bug report?

Check it out: make some particles that goes from the bottom of the screen to the top using screen space. Then, switch screen orientation to landscape and back to portrait. See what happens.

yaustar commented 2 years ago

No, not worth a new report as the particles screen space seems to be bugged generally

yaustar commented 2 years ago

I've made a fix that works perfectly well

var ParticleScalingFix = pc.createScript('particleScalingFix');

/** @type {pc.Vec3}*/
let originalScale;

// initialize code called once per entity
ParticleScalingFix.prototype.initialize = function() {

    originalScale =  { ...this.entity.getLocalScale() };

    this.app.graphicsDevice.on('resizecanvas', this.onResizeCanvas, this);
    this.onResizeCanvas(this.app.graphicsDevice.width, this.app.graphicsDevice.height);
};

ParticleScalingFix.prototype.onResizeCanvas = function(w, h) {
    this.entity.setLocalScale(originalScale.x, (w/h) * originalScale.y, originalScale.z);

};

Just need to attach the script to the particle system.

Does that work if the UI screen component blend value is not 0.5?

marcusx2 commented 2 years ago

I've made a fix that works perfectly well

var ParticleScalingFix = pc.createScript('particleScalingFix');

/** @type {pc.Vec3}*/
let originalScale;

// initialize code called once per entity
ParticleScalingFix.prototype.initialize = function() {

    originalScale =  { ...this.entity.getLocalScale() };

    this.app.graphicsDevice.on('resizecanvas', this.onResizeCanvas, this);
    this.onResizeCanvas(this.app.graphicsDevice.width, this.app.graphicsDevice.height);
};

ParticleScalingFix.prototype.onResizeCanvas = function(w, h) {
    this.entity.setLocalScale(originalScale.x, (w/h) * originalScale.y, originalScale.z);

};

Just need to attach the script to the particle system.

Does that work if the UI screen component blend value is not 0.5?

I'm using scale mode none. In fact the scale mode doesn't seem to make any difference. It's working perfectly now, except if the user switches the orientation of the phone, then it glitches a bit...but oh well. I think it's good enough now.

yaustar commented 2 years ago

I'm using scale mode none. In fact the scale mode doesn't seem to make any difference. It's working perfectly now, except if the user switches the orientation of the phone, then it glitches a bit...but oh well. I think it's good enough now.

It probably looks a bit odd as you are only scaling along one axis (Y)

marcusx2 commented 2 years ago

I'm using scale mode none. In fact the scale mode doesn't seem to make any difference. It's working perfectly now, except if the user switches the orientation of the phone, then it glitches a bit...but oh well. I think it's good enough now.

It probably looks a bit odd as you are only scaling along one axis (Y)

It doesn't look odd, it looks perfect. The scaling issue only happens on the Y axis. That is the formula to compensate for the stretch on the Y. Why don't you try it? You'll see.

yaustar commented 2 years ago

Are you using Screen Space and/or Local Space on the particle effect?

marcusx2 commented 2 years ago

Just Screen Space

marcusx2 commented 2 years ago
var ParticleScalingFix = pc.createScript('particleScalingFix');

/** @type {pc.Vec3}*/
let originalScale;

// initialize code called once per entity
ParticleScalingFix.prototype.initialize = function() {
    originalScale =  { ...this.entity.getLocalScale() };

    this.app.graphicsDevice.on('resizecanvas', this.onResizeCanvas, this);
    this.onResizeCanvas(this.app.graphicsDevice.width, this.app.graphicsDevice.height);
};

ParticleScalingFix.prototype.onResizeCanvas = function(w, h) {
    this.entity.setLocalScale(originalScale.x, (w/h) * originalScale.y, originalScale.z);
    this.entity.particlesystem.reset();
};

Calling this.entity.particlesystem.reset(); fixes the issue of the particles behaving weird when switching the orientation of the phone. Now everything works perfectly. This is the definitive fix for screenspace particles. Maybe you can simply incorporate this code in the particleSystem source? Don't forget to try it out.

To see the weird movement behavior I'm talking about, remove the line this.entity.particlesystem.reset();, then switch from portait to landscape, and back to portrait, and see what happens.

marcusx2 commented 2 years ago

Ok this is weird. I am using different particles now, and I noticed that the scaling can happen on the Y only, or the X and the Y. Probably only on the X as well. So I modified the script to let the user decide what axis he wants to apply the fix after testing.

var ParticleScalingFix = pc.createScript('particleScalingFix');

ParticleScalingFix.attributes.add('scaleY', { type: 'boolean' });
ParticleScalingFix.attributes.add('scaleX', { type: 'boolean' });

/** @type {pc.Vec3}*/
let originalScale;

// initialize code called once per entity
ParticleScalingFix.prototype.initialize = function() {
    originalScale =  { ...this.entity.getLocalScale() };

    this.app.graphicsDevice.on('resizecanvas', this.onResizeCanvas, this);
    this.onResizeCanvas(this.app.graphicsDevice.width, this.app.graphicsDevice.height);
};

ParticleScalingFix.prototype.onResizeCanvas = function(w, h) {
    this.entity.setLocalScale(this.scaleX ? h > w ? (h/w) * originalScale.x : (w/h) * originalScale.x : originalScale.x, this.scaleY ? (w/h) * originalScale.y : originalScale.y, originalScale.z);
    this.entity.particlesystem.reset();
};

In addition, when it needs to be scaled on the x axis, if the height of the viewport is bigger than the width, h/w needs to be used instead of w/h. The y axis is always w/h. I have no idea why it's like this, but it is.

marcusx2 commented 2 years ago

The reset trick doesn't work if I have a random scale. Does the reset not touch the scale of the particles? T_T. Another bug, this one I couldn't fix. If you have random scales, and switch from potrait to landscape, and then back to portrait, the particles become super small.

yaustar commented 2 years ago

Another bug, this one I couldn't fix. If you have random scales, and switch from potrait to landscape, and then back to portrait, the particles become super small.

Sounds like a separate issue with particles. Can you please post in the engine repo with a example project and steps to reproduce the issue?

marcusx2 commented 2 years ago

I can't reproduce the issue without screen space mode. I don't think it's a completely unrelated issue. On another note, do you know how can I check the width and height of the image being used by the particle system? Something like particleSystem.image.width

yaustar commented 2 years ago

You can get the texture that the particle system is using via https://developer.playcanvas.com/en/api/pc.ParticleSystemComponent.html#colorMap

However, I don't think it affects the render size and it's done via entity scale and particle scale graph

marcusx2 commented 2 years ago

It does affect how the stretching happens. If the width is bigger than the height, it stretches on the x and y. Otherwise, it stretches only in the y axis. No need to have user input to decide how to fix the stretch. Here's the updated code:

var ParticleScalingFix = pc.createScript('particleScalingFix');

/** @type {pc.Vec3}*/
let originalScale;

// initialize code called once per entity
ParticleScalingFix.prototype.initialize = function() {
    originalScale =  { ...this.entity.getLocalScale() };

    this.app.graphicsDevice.on('resizecanvas', this.onResizeCanvas, this);
    this.onResizeCanvas(this.app.graphicsDevice.width, this.app.graphicsDevice.height);
};

ParticleScalingFix.prototype.onResizeCanvas = function(w, h) {
    if (this.entity.particlesystem.colorMap.width > this.entity.particlesystem.colorMap.height)
        this.entity.setLocalScale(h > w ? (h/w) * originalScale.x : (w/h) * originalScale.x, (w/h) * originalScale.y, originalScale.z);
    else {
        this.entity.setLocalScale(originalScale.x, (w/h) * originalScale.y, originalScale.z);
    }

    this.entity.particlesystem.reset();
};
marcusx2 commented 2 years ago

@yaustar Hey yaustar nevermind. The bug of the particles getting super small was my fault. The originalScale variable needs to be in the prototype, otherwise it acts as a global variable that all particleScalingFix can access, instead of being a unique instance of the variable per script. Interesting. Here is the final script, this time I'm having no issues, tested with different particles of different heights and widths.

var ParticleScalingFix = pc.createScript('particleScalingFix');

/** @type {pc.Vec3}*/
ParticleScalingFix.prototype.originalScale = 0;

// initialize code called once per entity
ParticleScalingFix.prototype.initialize = function() {
    this.originalScale =  { ...this.entity.getLocalScale() };

    this.app.graphicsDevice.on('resizecanvas', this.onResizeCanvas, this);
    this.onResizeCanvas(this.app.graphicsDevice.width, this.app.graphicsDevice.height);
};

ParticleScalingFix.prototype.onResizeCanvas = function(w, h) {
    if (this.entity.particlesystem.colorMap.width > this.entity.particlesystem.colorMap.height) {
        this.entity.setLocalScale(h > w ? (h/w) * this.originalScale.x : (w/h) * this.originalScale.x, (w/h) * this.originalScale.y, this.originalScale.z);
    }
    else {
        this.entity.setLocalScale(this.originalScale.x, (w/h) * this.originalScale.y, this.originalScale.z);
    }

    this.entity.particlesystem.reset();
};

Just a reminder: when it needs to be scaled on the x axis, if the height of the viewport is bigger than the width, h/w needs to be used instead of w/h. The y axis is always w/h. I have no idea why it's like this, but it is.

If the width of the image is bigger than the height, the fix needs to be applied to the x and y axis, otherwise only the y.

yaustar commented 2 years ago

FYI, you don't need this line:

/** @type {pc.Vec3}*/
ParticleScalingFix.prototype.originalScale = 0;
marcusx2 commented 2 years ago

You think you can take a look at my patch? I wouldn't mind if this was used to fix screen space instead of a super complex solution.

yaustar commented 2 years ago

It's all recorded here in the ticket so the engine team can consider it when it comes to working on this issue.