2

I am trying to save an image to the server that is being created with the following three.js script..

actualCode(THREE);

function actualCode(THREE) {
    //Variables for rendering
    const renderer = new THREE.WebGLRenderer({
        antialias: true
    });
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(30, 400.0 / 400, 1, 1000);

    //Object variables
    let texture;
    let paintedMug;

    //Preload image, then trigger rendering
    const loader = new THREE.TextureLoader();
    texture = loader.load("images/djmug2.jpg", function (_tex) {
        // /*Debugging:*/ setTimeout(() => document.body.appendChild(texture.image), 100);
        init();

        //views 17.5=front | 355=side | 139.6=back
        renderImageSolo(17.5);

    });

    function init() {
        //Init scene and camera
        camera.position.set(0, 1.3, 11);
        camera.lookAt(scene.position);
        renderer.setSize(400, 400);
        
        //Set an ambient light
        const light = new THREE.AmbientLight(0xffffff); // soft white light
        scene.add(light);

        //Draw white mug
        const muggeom = new THREE.CylinderGeometry(1.5, 1.5, 3.5, 240, 1);
        const mugmaterial = new THREE.MeshStandardMaterial({
            color: "#fff",
        });
        const mug = new THREE.Mesh(muggeom, mugmaterial);

        //Draw painting on mug with slightly larger radius
        const paintgeom = new THREE.CylinderGeometry(1.5001, 1.5001, 3.3, 240, 1, true);
        const paintmaterial = new THREE.MeshStandardMaterial({
            map: texture,
        });
        const paint = new THREE.Mesh(paintgeom, paintmaterial);

        //Define a group as mug + paint
        paintedMug = new THREE.Group();
        paintedMug.add(mug);
        paintedMug.add(paint);
        //Add group to scene
        scene.add(paintedMug);
    }


    function renderImageSolo(angle) {
        //Init just like main renderer / scene, will use same camera
        const solo_renderer = new THREE.WebGLRenderer({
            antialias: true
        });
        solo_renderer.setSize(renderer.domElement.width, renderer.domElement.height);
        solo_renderer.domElement.style.marginTop = "0em"; //Space out canvas
        solo_renderer.domElement.id = "canvas"; //give canvas id
        document.body.appendChild(solo_renderer.domElement);
        const solo_scene = new THREE.Scene();
        //Set an ambient light
        const light = new THREE.AmbientLight(0xffffff); // soft white light
        solo_scene.add(light);

        //Draw painting alone
        const paintgeom = new THREE.CylinderGeometry(1.5, 1.5, 3.3, 240, 1, true);
        const paintmaterial = new THREE.MeshStandardMaterial({
            map: texture,
        });
        const paint = new THREE.Mesh(paintgeom, paintmaterial);
        //Add paint to scene
        solo_scene.add(paint);
        //Rotate paint by angle
        paint.rotation.y = angle
        //Draw result with green screen bg
        solo_scene.background = new THREE.Color(0x04F404);
        //Draw result with trans bg (not working showing as black atm)
        //solo_scene.background = new THREE.WebGLRenderer( { alpha: true } );

        solo_renderer.render(solo_scene, camera);
        saveit();
    }
}

I then attempt to save the generated image with ajax as follows..

function saveit() {
    const canvas = document.getElementById('canvas');
    var photo = canvas.toDataURL('image/jpeg');
    $.ajax({
        method: 'POST',
        url: 'photo_upload.php',
        data: {
            photo: photo
        }
    });
}

photo_upload.php contents..

$data = $_POST['photo'];
    list($type, $data) = explode(';', $data);
    list(, $data)      = explode(',', $data);
    $data = base64_decode($data);

    mkdir($_SERVER['DOCUMENT_ROOT'] . "/photos");

    file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/photos/".time().'.png', $data);
    die;

but nothing gets saved and /photos on the server remains empty, also, as a seperate issue if i right click and "save image" the saved image is just a black square and not what is shown on the screen.

Glen Keybit
  • 296
  • 2
  • 15

2 Answers2

1

Code for saving to PHP server re-written with modern javascript and tested:

  1. Keep only the relevant part of js and add the saving function using fetch
import * as THREE from 'https://cdn.skypack.dev/three';

document.addEventListener("DOMContentLoaded", _e => {

  //Create a div to receive results
  const messDiv = document.createElement('div');
  messDiv.classList.add('message');
  document.body.appendChild(messDiv);

  //Object variables
  let texture;

  //Preload image, then trigger rendering
  const loader = new THREE.TextureLoader();
  //Example with image hosted from Imgur:
  messDiv.textContent = "Loading texture...";
  texture = loader.load("https://i.imgur.com/TQZrUSP.jpeg", function(_tex) {
    console.log("texture loaded");
    // /*Debugging:*/ setTimeout(() => document.body.appendChild(texture.image), 100);
    renderImageSolo(60);
  });

  function renderImageSolo(angle) {
    messDiv.textContent = "Rendering 3D projection...";
    //Init just main renderer / scene
    const solo_renderer = new THREE.WebGLRenderer({
      antialias: true,
      preserveDrawingBuffer: true // <-- avoid plain black image
    });
    solo_renderer.setSize(400, 400);
    document.body.appendChild(solo_renderer.domElement);
    const solo_scene = new THREE.Scene();
    //Init camera
    const camera = new THREE.PerspectiveCamera(30, 400.0 / 400, 1, 1000);
    camera.position.set(0, 1.3, 11);
    camera.lookAt(solo_scene.position);
    //Set an ambient light
    const light = new THREE.AmbientLight(0xffffff); // soft white light
    solo_scene.add(light);

    //Draw painting alone
    const paintgeom = new THREE.CylinderGeometry(1.5, 1.5, 3.3, 240, 1, true);
    const paintmaterial = new THREE.MeshStandardMaterial({
      //color: "#ddd",
      map: texture,
    });
    const paint = new THREE.Mesh(paintgeom, paintmaterial);
    //Add paint to scene
    solo_scene.add(paint);
    //Rotate paint by angle
    paint.rotation.y = angle
    //Draw result
    solo_scene.background = new THREE.Color(0xffffff);
    solo_renderer.render(solo_scene, camera);
    //Save result
    saveImage(solo_renderer.domElement, "photo.jpeg")
  }

  //Save canvas as image by posting it to special url on server
  function saveImage(canvas, filename) {
    messDiv.textContent = "Uploading result...";

    canvas.toBlob(imgBlob => { //Specifying image/jpeg, otherwise you'd get a png
      const fileform = new FormData();
      fileform.append('filename', filename);
      fileform.append('data', imgBlob);
      fetch('./photo_upload.php', {
        method: 'POST',
        body: fileform,
      })
      .then(response => {
        return response.json();
      })
      .then(data => {
        if (data.error) { //Show server errors
          messDiv.classList.add('error');
          messDiv.textContent = data.error;
        } else { //Show success message
          messDiv.classList.add('success');
          messDiv.textContent = data.message;
        }
      })
      .catch(err => { //Handle js errors
        console.log(err);
        messDiv.classList.add('error');
        messDiv.textContent = err.message;
      });
    }, 'image/jpeg'); //<- image type for canvas.toBlob (defaults to png)
  }
});
  1. Write code to save on PHP server
<?php
//photo_upload.php

try {

  header('Content-type: application/json');

  //get file name
  $filename = $_POST['filename'];
  if (!$filename) {
    die(json_encode([
      'error' => "Could not read filename from request"
    ]));
  }
  //get image data
  $img = $_FILES['data'];
  if (!$filename) {
    die(json_encode([
      'error' => "No image data in request"
    ]));
  }
  //Create save dir
  $savePath = $_SERVER['DOCUMENT_ROOT'] . "/photos/";
  if (!file_exists($savePath)) {
    if (!mkdir($savePath)) {
      die(json_encode([
        'error' => "Could not create dir $savePath"
      ]));
    }
  }
  //Save file
  $savePath .= $filename;
  if (!move_uploaded_file($img['tmp_name'], $savePath)) {
    echo json_encode([
      'error' => "Could not write to $savePath"
    ]);
  } else {
    $bytes = filesize($savePath);
    echo json_encode([
      'message' => "Image uploaded and saved to $savePath ($bytes bytes)"
    ]);
  }

} catch (Exception $err) {
  echo json_encode([
    'error' => $err->getMessage()
  ]);
}
  1. A little CSS to make messages more readable
body {
  font-family: Arial, Helvetica, sans-serif;
}
.message {
  text-align: center;
  padding: 1em;
  font-style: italic;
  color: dimgray;
}
.message.success {
  font-style: normal;
  font-weight: bold;
  color: forestgreen;
}
.message.error {
  font-style: normal;
  font-family: 'Courier New', Courier, monospace;
  white-space: pre-wrap;
  color: darkred;
}

2021-09-07 - I've edited the code to use js FormData and PHP $_FILES for better efficiency and readbility

julien.giband
  • 2,467
  • 1
  • 12
  • 19
  • This is great, thank you! just one thing, how do I suppress/hide/get rid of the messages? such as message => "Image uploaded and saved to $savePath ($bytes bytes)" I don't want to have to click OK on a dialog box. Thanks in advance. – Glen Keybit Aug 31 '21 at 18:05
  • I'll let you search a little by yourself and learn something. I can point you to the JavaScript side, and let's say a popup message is shown with the `alert` function... – julien.giband Sep 01 '21 at 15:11
  • I tried for 3 hours yesterday and reseached as best i could, everytime i took a line out (or commented one out) I got an error pop up that replaced the alert. I did indeed search a little by myself and have also been learning a fair bit of Javascript/PHP and related over the past few years but I am still very new and 100 percent self-taught by trial and error. I actaully try my hardest (that time also allows) to solve somthing myself before asking for help. Thank you again for all of your help, it is very much appriciated. – Glen Keybit Sep 01 '21 at 16:20
  • I think I was editing the wrong file and trying to take the errors out of file-upload.php when its the alerts out on the js script I should have been looking at! My sincere apologies for being so wrapped up in the wrong file and missing the now obvious! – Glen Keybit Sep 01 '21 at 16:34
  • Congrats on solving it by yourself! I didn't event have time to notice your answers – julien.giband Sep 01 '21 at 16:36
  • 1
    Be careful though, as I tried to keep it simple and did not separate the server fail messages from the success message, so just hiding the alert may lead you to not notice some errors. You'd better replace the `alert` with a `console.log`, so you can at least easily read the server result in your browser's dev tools console – julien.giband Sep 01 '21 at 16:40
0

You should be able to solve this issue by creating the renderer like so:

const solo_renderer = new THREE.WebGLRenderer({
    antialias: true,
    preserveDrawingBuffer: true // FIX
});

I also suggest you study existing resources that explain how to save a screenshot of your canvas with. Try it with:

Three.js: How can I make a 2D SnapShot of a Scene as a JPG Image?

Mugen87
  • 28,829
  • 4
  • 27
  • 50
  • Thank you for your answer, preserveDrawingBuffer: true has fixed my second issue of it showing previously as a black square. However, most of the answers in the link you provided are about how to save the file locally and not to the server which is the part I am struggling with the most. – Glen Keybit Aug 31 '21 at 08:25