Skip to content

Commit bf925d7

Browse files
mvaligurskyMartin Valigurskywilleastcott
authored
Simple planar-renderer script to render planar reflections (playcanvas#5186)
* Simple planar-renderer script to render planar reflections + engine example * type fix * import * additional fix * updated plane class * clamp texture addressing * lint * Update src/core/shape/plane.js Co-authored-by: Will Eastcott <will@playcanvas.com> * not scaling buffer --------- Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com> Co-authored-by: Will Eastcott <will@playcanvas.com>
1 parent 47148aa commit bf925d7

File tree

11 files changed

+504
-56
lines changed

11 files changed

+504
-56
lines changed
292 KB
Loading

examples/src/examples/graphics/index.mjs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import AreaLightsExample from "./area-lights";
22
import AreaPickerExample from "./area-picker";
33
import AssetViewerExample from "./asset-viewer";
44
import BatchingDynamicExample from "./batching-dynamic";
5-
import BoxReflectionExample from "./box-reflection";
5+
import ReflectionBoxExample from "./reflection-box";
66
import ClusteredAreaLightsExample from "./clustered-area-lights";
77
import ClusteredLightingExample from "./clustered-lighting";
88
import ClusteredOmniShadowsExample from "./clustered-omni-shadows";
@@ -41,8 +41,9 @@ import PointCloudSimulationExample from "./point-cloud-simulation";
4141
import PointCloudExample from "./point-cloud";
4242
import PortalExample from "./portal";
4343
import PostEffectsExample from "./post-effects";
44+
import ReflectionPlanarExample from "./reflection-planar";
4445
import RenderAssetExample from "./render-asset";
45-
import RenderCubemapExample from "./render-cubemap";
46+
import ReflectionCubemapExample from "./reflection-cubemap";
4647
import RenderToTextureExample from "./render-to-texture";
4748
import ShaderBurnExample from "./shader-burn";
4849
import ShaderCompileExample from "./shader-compile";
@@ -59,7 +60,7 @@ export {
5960
AreaPickerExample,
6061
AssetViewerExample,
6162
BatchingDynamicExample,
62-
BoxReflectionExample,
63+
ReflectionBoxExample,
6364
ClusteredAreaLightsExample,
6465
ClusteredLightingExample,
6566
ClusteredOmniShadowsExample,
@@ -98,8 +99,9 @@ export {
9899
PointCloudExample,
99100
PortalExample,
100101
PostEffectsExample,
102+
ReflectionPlanarExample,
101103
RenderAssetExample,
102-
RenderCubemapExample,
104+
ReflectionCubemapExample,
103105
RenderToTextureExample,
104106
ShaderBurnExample,
105107
ShaderCompileExample,

examples/src/examples/graphics/box-reflection.tsx renamed to examples/src/examples/graphics/reflection-box.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import * as pc from '../../../../';
44
import { BindingTwoWay, LabelGroup, Panel, SelectInput, SliderInput } from '@playcanvas/pcui/react';
55
import { Observer } from '@playcanvas/observer';
66

7-
class BoxReflectionExample {
7+
class ReflectionBoxExample {
88
static CATEGORY = 'Graphics';
9-
static NAME = 'Box Reflection';
9+
static NAME = 'Reflection Box';
1010
static WEBGPU_ENABLED = true;
1111

1212
controls(data: Observer) {
@@ -397,4 +397,4 @@ class BoxReflectionExample {
397397
}
398398
}
399399

400-
export default BoxReflectionExample;
400+
export default ReflectionBoxExample;

examples/src/examples/graphics/render-cubemap.tsx renamed to examples/src/examples/graphics/reflection-cubemap.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as pc from '../../../../';
22

3-
class RenderCubemapExample {
3+
class ReflectionCubemapExample {
44
static CATEGORY = 'Graphics';
5-
static NAME = 'Render Cubemap';
5+
static NAME = 'Reflection Cubemap';
66
static WEBGPU_ENABLED = true;
77

88
example(canvas: HTMLCanvasElement, deviceType: string): void {
@@ -285,4 +285,4 @@ class RenderCubemapExample {
285285
}
286286
}
287287

288-
export default RenderCubemapExample;
288+
export default ReflectionCubemapExample;
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import * as pc from '../../../../';
2+
3+
class ReflectionPlanarExample {
4+
static CATEGORY = 'Graphics';
5+
static NAME = 'Reflection Planar';
6+
static WEBGPU_ENABLED = true;
7+
8+
static FILES = {
9+
'shader.vert': /* glsl */`
10+
attribute vec3 aPosition;
11+
attribute vec2 aUv0;
12+
13+
uniform mat4 matrix_model;
14+
uniform mat4 matrix_viewProjection;
15+
16+
void main(void)
17+
{
18+
gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0);;
19+
}`,
20+
21+
'shader.frag': /* glsl */`
22+
23+
// engine built-in constant storing render target size in .xy and inverse size in .zw
24+
uniform vec4 uScreenSize;
25+
26+
// reflection texture
27+
uniform sampler2D uDiffuseMap;
28+
29+
void main(void)
30+
{
31+
// sample reflection texture
32+
vec2 coord = gl_FragCoord.xy * uScreenSize.zw;
33+
coord.y = 1.0 - coord.y;
34+
vec4 reflection = texture2D(uDiffuseMap, coord);
35+
36+
gl_FragColor = vec4(reflection.xyz * 0.7, 1);
37+
}`
38+
};
39+
40+
example(canvas: HTMLCanvasElement, deviceType: string, files: { 'shader.vert': string, 'shader.frag': string }): void {
41+
42+
const assets = {
43+
envatlas: new pc.Asset('helipad-env-atlas', 'texture', { url: '/static/assets/cubemaps/dancing-hall-env-atlas.png' }, { type: pc.TEXTURETYPE_RGBP }),
44+
'statue': new pc.Asset('statue', 'container', { url: '/static/assets/models/statue.glb' }),
45+
'script': new pc.Asset('script', 'script', { url: '/static/scripts/utils/planar-renderer.js' })
46+
};
47+
48+
const gfxOptions = {
49+
deviceTypes: [deviceType],
50+
glslangUrl: '/static/lib/glslang/glslang.js',
51+
twgslUrl: '/static/lib/twgsl/twgsl.js'
52+
};
53+
54+
pc.createGraphicsDevice(canvas, gfxOptions).then((device: pc.GraphicsDevice) => {
55+
56+
const createOptions = new pc.AppOptions();
57+
createOptions.graphicsDevice = device;
58+
59+
createOptions.componentSystems = [
60+
// @ts-ignore
61+
pc.RenderComponentSystem,
62+
// @ts-ignore
63+
pc.CameraComponentSystem,
64+
// @ts-ignore
65+
pc.ScriptComponentSystem
66+
];
67+
createOptions.resourceHandlers = [
68+
// @ts-ignore
69+
pc.TextureHandler,
70+
// @ts-ignore
71+
pc.ScriptHandler,
72+
// @ts-ignore
73+
pc.ContainerHandler
74+
];
75+
76+
const app = new pc.AppBase(canvas);
77+
app.init(createOptions);
78+
79+
// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
80+
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
81+
app.setCanvasResolution(pc.RESOLUTION_AUTO);
82+
83+
const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
84+
assetListLoader.load(() => {
85+
86+
app.start();
87+
88+
// set up some general scene rendering properties
89+
app.scene.toneMapping = pc.TONEMAP_ACES;
90+
91+
// setup skydome
92+
app.scene.envAtlas = assets.envatlas.resource;
93+
app.scene.skyboxMip = 1;
94+
app.scene.skyboxIntensity = 1.2; // make it brighter
95+
96+
// helper function to create a primitive with shape type, position, scale, color and layer
97+
function createPrimitive(primitiveType: string, position: pc.Vec3, scale: pc.Vec3, color: pc.Color, layer: number[], material: pc.Material | pc.StandardMaterial = null) {
98+
99+
// create material of specified color
100+
if (!material) {
101+
const standardMaterial = new pc.StandardMaterial();
102+
standardMaterial.diffuse = color;
103+
standardMaterial.gloss = 0.6;
104+
standardMaterial.metalness = 0.7;
105+
standardMaterial.useMetalness = true;
106+
standardMaterial.update();
107+
material = standardMaterial;
108+
}
109+
110+
// create primitive
111+
const primitive = new pc.Entity();
112+
primitive.addComponent('render', {
113+
type: primitiveType,
114+
layers: layer,
115+
material: material
116+
});
117+
118+
// set position and scale and add it to scene
119+
primitive.setLocalPosition(position);
120+
primitive.setLocalScale(scale);
121+
app.root.addChild(primitive);
122+
123+
return primitive;
124+
}
125+
126+
// create a layer for objects that do not render into texture
127+
const excludedLayer = new pc.Layer({ name: "Excluded" });
128+
app.scene.layers.push(excludedLayer);
129+
130+
// get world and skybox layers
131+
const worldLayer = app.scene.layers.getLayerByName("World");
132+
const skyboxLayer = app.scene.layers.getLayerByName("Skybox");
133+
134+
// Create the shader from the vertex and fragment shaders
135+
const shader = pc.createShaderFromCode(app.graphicsDevice, files['shader.vert'], files['shader.frag'], 'myShader', {
136+
aPosition: pc.SEMANTIC_POSITION,
137+
aUv0: pc.SEMANTIC_TEXCOORD0
138+
});
139+
140+
// reflective ground
141+
// This is in the excluded layer so it does not render into reflection texture
142+
const groundMaterial = new pc.Material();
143+
groundMaterial.shader = shader;
144+
createPrimitive("plane", new pc.Vec3(0, 0, 0), new pc.Vec3(40, 1, 40), new pc.Color(0.5, 0.5, 0.5), [excludedLayer.id], groundMaterial);
145+
146+
// get the instance of the statue and set up with render component
147+
const statueEntity = assets.statue.resource.instantiateRenderEntity();
148+
app.root.addChild(statueEntity);
149+
150+
// create few random primitives in the world layer
151+
const entities: pc.Entity[] = [];
152+
const shapes = ["box", "cone", "cylinder", "sphere", "capsule"];
153+
for (let i = 0; i < 6; i++) {
154+
const shapeName = shapes[Math.floor(Math.random() * shapes.length)];
155+
const color = new pc.Color(Math.random(), Math.random(), Math.random());
156+
entities.push(createPrimitive(shapeName, pc.Vec3.ZERO, new pc.Vec3(3, 3, 3), color, [worldLayer.id]));
157+
}
158+
159+
// Create main camera, which renders entities in world, excluded and skybox layers
160+
const camera = new pc.Entity("MainCamera");
161+
camera.addComponent("camera", {
162+
fov: 60,
163+
layers: [worldLayer.id, excludedLayer.id, skyboxLayer.id]
164+
});
165+
app.root.addChild(camera);
166+
167+
// create reflection camera, which renders entities in world and skybox layers only
168+
const reflectionCamera = new pc.Entity("ReflectionCamera");
169+
reflectionCamera.addComponent("camera", {
170+
fov: 60,
171+
layers: [worldLayer.id, skyboxLayer.id],
172+
priority: -1 // render reflections before the main camera
173+
});
174+
175+
// add planarRenderer script which renders the reflection texture
176+
reflectionCamera.addComponent('script');
177+
reflectionCamera.script.create('planarRenderer', {
178+
attributes: {
179+
sceneCameraEntity: camera,
180+
scale: 1,
181+
mipmaps: false,
182+
depth: true,
183+
planePoint: pc.Vec3.ZERO,
184+
planeNormal: pc.Vec3.UP
185+
}
186+
});
187+
app.root.addChild(reflectionCamera);
188+
189+
// update things each frame
190+
let time = 0;
191+
app.on("update", function (dt) {
192+
time += dt;
193+
194+
// rotate primitives around their center and also orbit them around the shiny sphere
195+
for (let e = 0; e < entities.length; e++) {
196+
const scale = (e + 1) / entities.length;
197+
const offset = time + e * 200;
198+
entities[e].setLocalPosition(7 * Math.sin(offset), e + 5, 7 * Math.cos(offset));
199+
entities[e].rotate(1 * scale, 2 * scale, 3 * scale);
200+
}
201+
202+
// slowly orbit camera around
203+
camera.setLocalPosition(30 * Math.cos(time * 0.2), 10, 30 * Math.sin(time * 0.2));
204+
camera.lookAt(pc.Vec3.ZERO);
205+
206+
// animate FOV
207+
camera.camera.fov = 60 + 20 * Math.sin(time * 0.5);
208+
209+
// trigger reflection camera update (must be called after all parameters of the main camera are updated)
210+
// @ts-ignore engine-tsd
211+
const reflectionTexture = reflectionCamera.script.planarRenderer.frameUpdate();
212+
groundMaterial.setParameter('uDiffuseMap', reflectionTexture);
213+
groundMaterial.update();
214+
});
215+
});
216+
});
217+
}
218+
}
219+
220+
export default ReflectionPlanarExample;

scripts/utils/cubemap-renderer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ CubemapRenderer.prototype.initialize = function () {
117117
}
118118

119119
// When last camera is finished rendering, trigger onCubemapPostRender event on the entity.
120-
// This can be listened to by the user, and the resuling cubemap can be further processed (e.g prefiltered)
120+
// This can be listened to by the user, and the resulting cubemap can be further processed (e.g prefiltered)
121121
if (i === 5) {
122122
e.camera.onPostRender = () => {
123123
this.entity.fire('onCubemapPostRender');

0 commit comments

Comments
 (0)