はじめに
- この記事でやること
- three.js の温室シーンに対して
PPFD(µmol/m²/s)の空間分布を計算するモデルを実装 - 各ライトについて
- 電力 [W]
- 光合成光量子束効率(PPE, µmol/J)
- ビーム角(照射角)
- 取り付け高さ(Y位置)
をパラメータとして持ち、
ベンチ上の格子点で PPFD を数値計算して
- 各ベンチごとに平均・最小・最大 PPFD を表示
- ライトの 照射角(ビーム角)と高さを UI から変更可能
- 数値の根拠・式の由来・近似は、論文・技術資料を引用して明記
2.科学的な前提:PAR, PPF, PPFD と効率
2.1 PAR, PPF, PPFD の定義
- PAR(Photosynthetically Active Radiation)
植物が光合成に利用できる 400–700 nm の波長帯の光を指す概念。 - PPF(Photosynthetic Photon Flux, µmol/s)
ランプから 1 秒間に放出される PAR フォトンの総数。
「PAR の総量」を表すフラックスで、単位は µmol/s。 - PPFD(Photosynthetic Photon Flux Density, µmol/m²/s)
単位面積あたり・単位時間あたりに到達する PAR フォトン数。
つまり「植物の葉に実際に届いている光子の密度」。
PPF と PPFD の関係は:
- PPF…光源側のトータル放出量(どれだけ出ているか)
- PPFD…作物面に実際に「落ちている量(密度)」
という関係です。
2.2 電力から PPF への変換:光合成光量子束効率(PPE)
園芸用照明では、電力 1 J あたり何 µmol の光子を出せるかが重要で、
これを PPE(Photosynthetic Photon Efficacy, µmol/J) と呼びます。
したがって、る光源の電力 P [W] と PPE𝜂[µmol/J] が分かれば、

計算できます。
文献の代表的な値:
-
高圧ナトリウムランプ(HPS)*
1.7〜1.85 µmol/J 程度が典型的。 -
近年の園芸用 LED
商用製品で 2.5〜3.5 µmol/J 程度、理論上は 4.6〜5.1 µmol/J まで可能とされる。
この記事のコードでは,光源毎に PPE の代表値をテーブルに持ち、PPF = 電力 × PPEで計算します。
3. PPFD 分布の数理モデル
3.1 スポットライトと立体角
ライトを「一定のビーム角をもつスポットライト」とみなし、その光は「円錐形の立体角 Ω」に一様に分布していると仮定します。
円錐の半頂角(中心軸から縁までの角度)を θ とすると、立体角 Ω は

で与えられます。これは立体角の標準的な公式であり、物理の教科書や技術資料に現れます。
ここで:
- コードの UI で扱う 「照射角(全角)」 を α [deg] とし、半頂角 𝜃=𝛼/2 [rad] とします。
3.2 光子強度(photonic intensity)
PPF(光子フラックス)を Φ [µmol/s] とし、それが円錐の立体角 Ω に一様に分布していると仮定すると、
光子強度(単位立体角あたりの光子数)は

となります。
※ 実際のランプは角度によって配光が変わる(Lambert 則など)ため、
ここは 「円錐内で一様」 という近似を明示しています。
モデル化としては単純ですが、式自体は立体角からの正しい導出です。
3.3 点ごとの PPFD(逆二乗則と余弦則)
ライト位置を 𝐿=(𝑥𝐿,𝑦𝐿,𝑧𝐿)、
ベンチ上の点を 𝑃=(𝑥𝑃,𝑦𝑃,𝑧𝑃) とします。
とおくと、ビーム中心軸と点方向とのなす角 𝜃beamは

-もし 𝜃beam>𝜃(半頂角より外)なら、その点はビーム外 → PPFD=0とみなします。
ベンチの表面は水平面なので、法線ベクトル n=(0,1,0)(上向き)とし、
点からライトへの方向ベクトルを

とすると、入射角 𝜃surf は

です。
余弦則(Lambert の余弦則)により、面に入るフラックスはこの cos𝜃surf に比例します。
したがって、あるライトから点 𝑃に入射する PPFD の寄与は

ここで I=Φ/Ω です。
これは 逆二乗則(1/r²)+余弦則を組み合わせた標準的な照度/放射照度の式であり、
光源が円錐立体角内で一様に光子を放出しているという仮定のもとで導かれます。
4. ベンチ上での PPFD サンプリングと評価
各ベンチを
- 幅 W=15 m(X 方向)
- 奥行き D=4 m(Z 方向)
- 上面の高さ 𝑦top≈0.55
として、上面を格子状に分割します(例:X 方向 15 点、Z 方向 5 点)。 - 各格子点で PPFD を計算
- ベンチ単位で
- 平均 PPFD
- 最小 PPFD
- 最大 PPFD
を算出し、UI に表として表示します。
参考までに、
レタス栽培でよく推奨される PPFD のレンジは、おおよそ 150〜300 µmol/m²/s 程度と報告されています。
5. ライト種別ごとのパラメータ(PPE・電力・寿命)
コード内では、光源ごとに以下のような代表値を持ちます。
- powerW : 電力 [W]
- efficacyUmolPerJ : PPE [µmol/J]
- lifetimeHours : 寿命 [h]
- defaultBeamDeg : 代表的なビーム角 [deg]
PPE の値は、上記の文献にあるレンジの中から代表値を選んだものです(個々の器具で変わる)。
6. フルコード(HTML + CSS + JavaScript)
ここからは そのまま index.html として動く 完全版コードです。(three.js は CDN r148 を使用)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>施設園芸ライト配置 & PPFDシミュレータ(three.js)</title>
<style>
body { margin: 0; font-family: sans-serif; }
#ui {
position: absolute;
top: 10px; left: 10px;
background: rgba(255, 255, 255, 0.96);
padding: 10px;
border-radius: 5px;
z-index: 10;
max-width: 380px;
font-size: 13px;
}
label { display: block; margin-top: 8px; font-weight: bold; }
input[type="range"], select { width: 100%; }
h3 { margin: 10px 0 4px 0; font-size: 14px; }
p.note { font-size: 11px; margin: 2px 0 6px 0; color: #555; }
table { font-size: 11px; border-collapse: collapse; margin-top: 4px; }
table th, table td { border: 1px solid #999; padding: 2px 4px; text-align: right; }
table th { background: #eee; text-align: center; }
</style>
</head>
<body>
<div id="ui">
<h3>カメラ視点調整</h3>
<p class="note">
X:左右から回り込む / Y:高さ(見下ろし) / Z:前後(ズーム)
</p>
<label>視点X:
<input type="range" id="camX" min="-80" max="80" value="0">
</label>
<label>視点Y:
<input type="range" id="camY" min="5" max="80" value="25">
</label>
<label>視点Z:
<input type="range" id="camZ" min="20" max="160" value="80">
</label>
<h3>ベンチ操作</h3>
<p class="note">赤い半透明がプレビュー。位置を決めて「ベンチ追加」。</p>
<label>ベンチX位置:
<input type="range" id="benchX" min="-30" max="30" value="0">
</label>
<label>ベンチZ位置:
<input type="range" id="benchZ" min="-10" max="10" value="0">
</label>
<button id="addBench">ベンチを追加</button>
<p id="benchCount">ベンチの数: 0</p>
<h3>ライト操作</h3>
<p class="note">黄色の球がプレビュー。位置・高さ・照射角を調整して「ライト追加」。</p>
<label>ライトX位置:
<input type="range" id="lightX" min="-30" max="30" value="0">
</label>
<label>ライトZ位置:
<input type="range" id="lightZ" min="-10" max="10" value="0">
</label>
<label>ライト高さY (m):
<input type="range" id="lightY" min="1" max="20" value="10" step="0.5">
</label>
<label>照射角(全角, 度):
<input type="range" id="lightBeam" min="40" max="140" value="120">
</label>
<h3>光源パラメータ</h3>
<label>
光源種別:
<select id="lightSource">
<option value="whiteLED">白色LED(栽培用)</option>
<option value="redLED">赤色LED 660 nm</option>
<option value="fluorescent">白色蛍光灯 FL (40W)</option>
<option value="HPS">高圧Naランプ HPSL (180W)</option>
<option value="MHL">メタハラランプ MHL (150W)</option>
<option value="CCFL">白色冷陰極管 CCFL (10W)</option>
</select>
</label>
<p class="note">
電力 × PPE(µmol/J) から PPF(µmol/s) を計算し、<br>
円錐ビーム内の PPFD を逆二乗則+余弦則で評価します。
</p>
<label>1日あたり使用時間 (h):
<input type="range" id="usageHours" min="1" max="24" value="16">
</label>
<p id="lightInfo"></p>
<button id="addLight">ライトを追加</button>
<p id="lightCount">ライトの数: 0</p>
<h3>PPFD 計算</h3>
<button id="recalcPPFD">PPFDを再計算</button>
<p class="note">
各ベンチ上面を格子状にサンプリングし、<br>
PPFD(µmol/m²/s)の平均・最小・最大を表示します。<br>
※ モデルは理論式に基づきますが、実機とは誤差が出ます。
</p>
<div id="ppfdOutput"></div>
<h3>参考</h3>
<p class="note">
例:レタスでは多くの研究で<br>
150〜300 µmol/m²/s 程度が適正範囲と報告されています。
</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.148.0/build/three.min.js"></script>
<script>
// ========= three.js シーン初期化 =========
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xd3d3d3);
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// ---- 床(温室の床 / 栽培エリア) ----
const floor = new THREE.Mesh(
new THREE.BoxGeometry(80, 0.1, 30),
new THREE.MeshLambertMaterial({ color: 0x228B22 })
);
floor.position.set(0, 0, 0);
scene.add(floor);
// ---- 環境光(室内のベース照明) ----
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
// ========= ベンチ関連 =========
const benchGeometry = new THREE.BoxGeometry(15, 0.5, 4);
const benchMaterial = new THREE.MeshStandardMaterial({
color: 0x8B4513,
roughness: 0.6,
metalness: 0.1
});
const benchPreviewMaterial = new THREE.MeshStandardMaterial({
color: 0xff0000,
opacity: 0.5,
transparent: true
});
const benchPreview = new THREE.Mesh(benchGeometry, benchPreviewMaterial);
benchPreview.position.set(0, 0.3, 0);
scene.add(benchPreview);
const benches = []; // { mesh, width, depth, topY }
// ========= ライト関連 =========
const lightPreview = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16),
new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.5 })
);
lightPreview.position.set(0, 10, 0);
scene.add(lightPreview);
// 物理パラメータテーブル
// ※ 値は文献にあるレンジからの代表値(実機依存)
const lightTypeData = {
whiteLED: {
label: "白色LED(栽培用)",
powerW: 200, // 例: 200 W クラス
efficacyUmolPerJ: 2.3, // µmol/J 付近(白色+赤混合の代表値)
lifetimeHours: 50000,
defaultBeamDeg: 120
},
redLED: {
label: "赤色LED 660 nm",
powerW: 100,
efficacyUmolPerJ: 3.0, // 赤単色は 3 µmol/J 前後が実用域の代表値
lifetimeHours: 80000,
defaultBeamDeg: 120
},
fluorescent: {
label: "白色蛍光灯 FL (40W)",
powerW: 40,
efficacyUmolPerJ: 0.9, // 一般蛍光灯のPPEの代表値(~1未満)
lifetimeHours: 12000,
defaultBeamDeg: 180 // 拡散的
},
HPS: {
label: "高圧Naランプ HPSL (180W)",
powerW: 180,
efficacyUmolPerJ: 1.8, // 文献レンジ 1.7〜1.85 µmol/J の代表値
lifetimeHours: 24000,
defaultBeamDeg: 120
},
MHL: {
label: "メタハラランプ MHL (150W)",
powerW: 150,
efficacyUmolPerJ: 1.2, // メタハラはHPSよりやや低効率
lifetimeHours: 10000,
defaultBeamDeg: 120
},
CCFL: {
label: "白色冷陰極管 CCFL (10W)",
powerW: 10,
efficacyUmolPerJ: 0.8, // 小型蛍光ランプ相当の代表値
lifetimeHours: 50000,
defaultBeamDeg: 180
}
};
const simLights = []; // { threeLight, marker, powerW, efficacyUmolPerJ, ppf, beamAngleDeg, halfAngleRad, cosHalfAngle }
// ========= DOM 取得 =========
const camX = document.getElementById("camX");
const camY = document.getElementById("camY");
const camZ = document.getElementById("camZ");
const benchX = document.getElementById("benchX");
const benchZ = document.getElementById("benchZ");
const addBenchBtn = document.getElementById("addBench");
const benchCountLabel = document.getElementById("benchCount");
const lightX = document.getElementById("lightX");
const lightZ = document.getElementById("lightZ");
const lightY = document.getElementById("lightY");
const lightBeam = document.getElementById("lightBeam");
const lightSource = document.getElementById("lightSource");
const usageHours = document.getElementById("usageHours");
const lightInfo = document.getElementById("lightInfo");
const lightCountLabel = document.getElementById("lightCount");
const addLightBtn = document.getElementById("addLight");
const recalcPPFDBtn = document.getElementById("recalcPPFD");
const ppfdOutput = document.getElementById("ppfdOutput");
// ========= カメラ更新 =========
function updateCamera() {
const x = Number(camX.value);
const y = Number(camY.value);
const z = Number(camZ.value);
camera.position.set(x, y, z);
camera.lookAt(0, 0.3, 0); // ベンチ高さあたりを見る
}
camX.addEventListener("input", updateCamera);
camY.addEventListener("input", updateCamera);
camZ.addEventListener("input", updateCamera);
// ========= ベンチプレビュー更新 =========
function updateBenchPreview() {
const x = Number(benchX.value);
const z = Number(benchZ.value);
benchPreview.position.set(x, 0.3, z);
}
benchX.addEventListener("input", updateBenchPreview);
benchZ.addEventListener("input", updateBenchPreview);
// ========= ライトプレビュー更新 =========
function updateLightPreview() {
const x = Number(lightX.value);
const z = Number(lightZ.value);
const y = Number(lightY.value);
lightPreview.position.set(x, y, z);
}
lightX.addEventListener("input", updateLightPreview);
lightZ.addEventListener("input", updateLightPreview);
lightY.addEventListener("input", updateLightPreview);
// ========= 光源情報の表示 =========
function updateLightInfo() {
const typeKey = lightSource.value;
const data = lightTypeData[typeKey];
const hoursPerDay = Number(usageHours.value);
const power = data.powerW; // W
const efficacy = data.efficacyUmolPerJ; // µmol/J
const ppf = power * efficacy; // µmol/s
const lifetime = data.lifetimeHours; // h
const days = Math.floor(lifetime / hoursPerDay);
lightInfo.innerHTML =
`<b>${data.label}</b><br>` +
`電力: ${power.toFixed(0)} W<br>` +
`PPE: ${efficacy.toFixed(2)} µmol/J<br>` +
`PPF: ${ppf.toFixed(1)} µmol/s (電力×PPE)<br>` +
`定格寿命: 約 ${lifetime.toLocaleString()} h (理論上 ${days.toLocaleString()} 日 @ ${hoursPerDay} h/日)`;
}
lightSource.addEventListener("change", updateLightInfo);
usageHours.addEventListener("input", updateLightInfo);
// ========= ベンチ追加 =========
function addBench() {
const bench = new THREE.Mesh(benchGeometry, benchMaterial.clone());
bench.position.copy(benchPreview.position);
scene.add(bench);
// BoxGeometry(15,0.5,4) を bench.position.y=0.3 に置くと上面は 0.3 + 0.25 = 0.55
const topY = bench.position.y + 0.25;
benches.push({
mesh: bench,
width: 15,
depth: 4,
topY: topY
});
benchCountLabel.textContent = `ベンチの数: ${benches.length}`;
recomputePPFD();
}
addBenchBtn.addEventListener("click", addBench);
// ========= ライト追加 =========
function addLight() {
const typeKey = lightSource.value;
const data = lightTypeData[typeKey];
const power = data.powerW;
const efficacy = data.efficacyUmolPerJ;
const ppf = power * efficacy; // µmol/s
const beamDeg = Number(lightBeam.value);
const halfAngleRad = THREE.MathUtils.degToRad(beamDeg / 2);
const cosHalfAngle = Math.cos(halfAngleRad);
const x = Number(lightX.value);
const z = Number(lightZ.value);
const y = Number(lightY.value);
const threeLight = new THREE.SpotLight(0xffffff, 1.0);
threeLight.position.set(x, y, z);
threeLight.angle = halfAngleRad;
threeLight.penumbra = 0.2;
threeLight.decay = 2;
threeLight.distance = 200;
// 下向きに照射するようにターゲットを設定
const target = new THREE.Object3D();
target.position.set(x, 0, z);
scene.add(target);
threeLight.target = target;
scene.add(threeLight);
// 可視化用の球
const marker = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16),
new THREE.MeshBasicMaterial({ color: 0xffff00 })
);
marker.position.copy(threeLight.position);
scene.add(marker);
simLights.push({
threeLight,
marker,
powerW: power,
efficacyUmolPerJ: efficacy,
ppf,
beamAngleDeg: beamDeg,
halfAngleRad,
cosHalfAngle
});
lightCountLabel.textContent = `ライトの数: ${simLights.length}`;
recomputePPFD();
}
addLightBtn.addEventListener("click", addLight);
lightBeam.addEventListener("input", () => {
// プレビューのビーム角説明用(実ビーム可視化は 3D 表現に依存)
// ここではテキストだけにしておく
});
// ========= PPFD 計算本体 =========
// ある空間点 (x,y,z) における PPFD を計算
// 単位: µmol/m^2/s
function computePPFDAtPoint(x, y, z) {
if (simLights.length === 0) return 0;
let total = 0;
// 法線ベクトル(ベンチ上面 = 上向き)
const nx = 0, ny = 1, nz = 0;
for (const L of simLights) {
const pos = L.threeLight.position;
const dx = x - pos.x;
const dy = y - pos.y;
const dz = z - pos.z;
const r2 = dx*dx + dy*dy + dz*dz;
if (r2 < 1e-6) continue; // ほぼゼロ距離は無視
const r = Math.sqrt(r2);
// ライトの照射軸は「下向き (0,-1,0)」で近似
const ax = 0, ay = -1, az = 0;
// ライト -> 点 方向ベクトル
const vx = dx / r;
const vy = dy / r;
const vz = dz / r;
// ビーム内判定
const cosThetaBeam = vx*ax + vy*ay + vz*az; // = cos(θ_beam)
if (cosThetaBeam <= L.cosHalfAngle) {
continue; // ビーム外 → このライトからは光が当たらないとみなす
}
// 点 -> ライト方向ベクトル
const px = -dx / r;
const py = -dy / r;
const pz = -dz / r;
// ベンチ法線との角度(Lambert余弦則用)
const cosThetaSurf = nx*px + ny*py + nz*pz; // cos(θ_surf)
if (cosThetaSurf <= 0) {
continue; // 面の裏側からの照射なので無視
}
// 立体角 Ω
const omega = 2 * Math.PI * (1 - L.cosHalfAngle); // sr
// 光子強度 I [µmol/s/sr]
const intensity = L.ppf / omega;
// PPFD = I * cosθ / r^2 [µmol / (m^2 s)]
const ppfd = intensity * cosThetaSurf / r2;
total += ppfd;
}
return total;
}
// 各ベンチ上面で PPFD をサンプリングし、結果を UI に表示
function recomputePPFD() {
if (benches.length === 0 || simLights.length === 0) {
ppfdOutput.innerHTML = "<p>ベンチまたはライトがありません。</p>";
return;
}
const samplesX = 15;
const samplesZ = 5;
const benchResults = [];
let globalMin = Infinity;
let globalMax = 0;
let globalSum = 0;
let globalCount = 0;
benches.forEach((bench, index) => {
const cx = bench.mesh.position.x;
const cz = bench.mesh.position.z;
const y = bench.topY;
const halfW = bench.width / 2;
const halfD = bench.depth / 2;
let sum = 0;
let min = Infinity;
let max = 0;
let count = 0;
for (let ix = 0; ix < samplesX; ix++) {
const x = cx - halfW + bench.width * (ix + 0.5) / samplesX;
for (let iz = 0; iz < samplesZ; iz++) {
const z = cz - halfD + bench.depth * (iz + 0.5) / samplesZ;
const v = computePPFDAtPoint(x, y, z);
if (!isFinite(v)) continue;
sum += v;
min = Math.min(min, v);
max = Math.max(max, v);
count++;
}
}
if (count > 0) {
const mean = sum / count;
benchResults.push({
index: index + 1,
mean,
min,
max
});
globalMin = Math.min(globalMin, min);
globalMax = Math.max(globalMax, max);
globalSum += sum;
globalCount += count;
}
});
let html = "<b>各ベンチ上面の PPFD 推定値 (µmol/m²/s)</b>";
html += "<table><tr><th>ベンチ</th><th>平均</th><th>最小</th><th>最大</th></tr>";
benchResults.forEach(r => {
html += `<tr>
<td style="text-align:center;">#${r.index}</td>
<td>${r.mean.toFixed(1)}</td>
<td>${r.min.toFixed(1)}</td>
<td>${r.max.toFixed(1)}</td>
</tr>`;
});
html += "</table>";
if (globalCount > 0) {
const gMean = globalSum / globalCount;
html += `<p>全ベンチ合算: 平均 ${gMean.toFixed(1)} / 最小 ${globalMin.toFixed(1)} / 最大 ${globalMax.toFixed(1)} µmol/m²/s</p>`;
html += `<p class="note">
目安:レタス人工光栽培では、150〜300 µmol/m²/s 程度が推奨レンジとする報告が多いです。
</p>`;
}
ppfdOutput.innerHTML = html;
}
recalcPPFDBtn.addEventListener("click", recomputePPFD);
// ========= アニメーションループ =========
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
// ========= リサイズ対応 =========
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 初期化
updateCamera();
updateBenchPreview();
updateLightPreview();
updateLightInfo();
animate();
</script>
</body>
</html>
7. このモデルの「科学的に正しい」範囲と限界
正しい(式として妥当)ところ
- PAR / PPF / PPFD / PPE の定義と単位は、園芸照明の標準的な定義に基づいています。
- PPF = 電力 × PPEは、PPE 定義からの直接の帰結です。
- 円錐ビームの立体角 Ω=2𝜋(1−cos𝜃) と、PPFD = PPF/Ω × cosθ / r² の形は、立体角+逆二乗則+余弦則から導かれる標準的な式です
近似・限界
- 各光源は 単一の点光源+円錐ビームで一様配光 とみなしている
- 実際には器具ごとに複雑な配光特性(Lambertian, 指向性レンズ 等)があり、
ここではモデル化のために「円錐内は一様、外は 0」という近似をしています。
- 実際には器具ごとに複雑な配光特性(Lambertian, 指向性レンズ 等)があり、
- スペクトル(波長分布)は考慮していない
- PPE として「PAR フォトン数」だけを見ており、
赤:青:遠赤などのスペクトル配分は無視しています。
- PPE として「PAR フォトン数」だけを見ており、
- 反射・散乱を無視(直射成分のみ)
- 温室の壁・床・ベンチの反射光は考慮していません。
つまり、「数式と実装ロジックは光学的に筋が通っている」が、実機の PPFD 分布を**mm 単位で再現する“完全シミュレーション”**ではない
というスタンスです。
8. この先さらに科学的に寄せるなら
もし、ここからさらに精度を上げたいなら:
- 実際の器具の PPFD マップ(実測データ) を取り込み、位置ごとの補正係数マップとして three.js に貼る
- 配光曲線(IES ファイル等)を読み込み、
角度ごとの光子強度 I(θ,ϕ) を使った積分にする - スペクトルサブモデルを用意して、
赤/青/緑/遠赤ごとの PPFD を別々に計算・表示する
などが考えられます。





